Почему порядок компиляции иногда вызывает ошибку сегментации при использовании std :: map :: insert ()?

У меня есть класс Controller, внутри которого есть класс Button. Controller содержит несколько Button экземпляров разных типов (например, button_type_a, button_type_b).


controller.h

#ifndef __controller__
#define __controller__
class Controller
{
public:
    class Button
    {
    public:
        Button(int type = -1);

    private:
        int type;
    };

    Controller();

    Button A;
    Button B;
    Button X;
    Button Y;
};
#endif


Типы кнопок ints, и я хотел бы иметь возможность связать определенные типы кнопок ints с указателями на Button экземпляров этих конкретных типов.

Чтобы отслеживать эту ассоциацию, я использую std::map<int, Controller::Button*>, который я typedef на buttonmap_t.

Когда я создаю новые Button экземпляры (в конструкторе Controller), конструктор Button регистрирует типы этих Button с картой.


controller.cpp

#include "controller.h"
#include <map>

typedef std::map<int, Controller::Button*> buttonmap_t;
buttonmap_t map;

Controller::Controller() :
A(0),
B(1),
X(2),
Y(3)
{ }

Controller::Button::Button(int type) :
type(type)
{
    map[type] = this;
}


Затем я создаю глобальный объект Controller и определяю main().


main.cpp

#include <iostream>
#include "controller.h"

Controller controller;

int main(int argc, const char * argv[])
{
    std::cout << "running..." << std::endl;
    return 0;
}


В зависимости от порядка, в котором я компилирую исходники, программа либо работает нормально, либо вызывает ошибку сегментации:

apogee:MapTest$ gcc controller.cpp main.cpp -o maptest -lstdc++
apogee:MapTest$ ./maptest 
running...

apogee:MapTest$ gcc main.cpp controller.cpp -o maptest -lstdc++
apogee:MapTest$ ./maptest 
Segmentation fault: 11


Похоже, что последний случай пытается использовать карту до того, как она была должным образом инициализирована, и это вызывает ошибку сегмента. Когда я отлаживаю с помощью Xcode, отладчик останавливается на «__tree», поскольку std::map вызывает __insert_node_at(), который выдает EXC_BAD_ACCESS(code=1, address=0x0). Стек вызовов показывает, что это было инициировано первым Button экземпляром, вызвавшим map[type] = this;.

Итак, вот мой вопрос, состоящий из нескольких частей:

  1. Почему это происходит из-за порядка компиляции?
  2. Есть ли способ добиться такого преобразования int в Button*, на которое не влияет порядок компиляции?
  3. Если так, то, что это?

В идеале я бы по-прежнему хотел, чтобы весь код, связанный с Controller и Button, был в отдельных файлах controller. *.


Похоже, это как-то связано (но не совсем так) со следующими вопросами:

  1. Ошибка сегментации в std :: map :: insert (...)
  2. Оператор [] в std :: map выдает ошибку сегментации

person camall3n    schedule 12.07.2013    source источник
comment
добавленное примечание, вы можете поместить коды в файл .h в директиву компилятора, которая скомпилирует его только один раз. было бы полезно использовать ifndef.   -  person Shamim Hafiz    schedule 13.07.2013
comment
Спасибо. Я удалил их для краткости, но просто добавил их обратно.   -  person camall3n    schedule 13.07.2013


Ответы (3)


Порядок инициализации статических переменных не определен, поэтому он зависит от вашей конкретной настройки, включая компилятор, компоновщик и порядок связывания.

Уловка с функцией инициализатора

Вы можете использовать уловку с функцией инициализатора, чтобы гарантировать, что что-то инициализируется при необходимости. Я знаю, что это наверняка работает в Windows с компилятором Microsoft C ++ (и провел небольшое тестирование на g ++ с Linux, см. Ниже).

Функция инициализатора

Первый шаг - переместить карту в функцию как статическую переменную и всегда обращаться к карте через эту функцию.

buttonmap_t& buttonMap() {
  static buttonmap_t map;
  return map;
}

использование

Карта создается при первом вызове функции buttonMap(). Если вы получаете доступ к карте через функцию, вы можете быть уверены, что она будет создана.

Controller::Button::Button(int type) :
  type_(type) {
    buttonMap()[type] = this;
}

Важнейшей частью является инициализация глобальной переменной: вы заменяете ее ссылкой и инициализируете ее из функции, которая содержит переменную.

buttonmap_t& map = buttonMap();

Объяснение

При такой настройке порядок инициализации не имеет значения, потому что первый вызов функции будет выполнять инициализацию, а каждый вызов после нее будет использовать инициализированный экземпляр.

Примечание: этот трюк работает для глобальных переменных, поскольку этап инициализации выполняется в одном потоке. Даже если вы не знаете точный порядок инициализации, вы можете быть уверены, что это произойдет последовательно.

Тестирование

Я тестировал это на своем домашнем компьютере с g ++ в Linux, и, похоже, он работает:

$ g++ main.cpp controller.cpp -Wall
$ ./a.out
running...

финальная программа:

// controller.h
#ifndef CONTROLLER_H
#define CONTROLLER_H

class Controller {
 public:
  class Button {
   public:
    Button(int type = -1);

   private:
    int type_;
  };

  Controller();

  Button A;
  Button B;
  Button X;
  Button Y;
};

#endif

// controller.cpp
#include "controller.h"
#include <map>

typedef std::map<int, Controller::Button*> buttonmap_t;

buttonmap_t& ButtonMap() {
  static buttonmap_t map;
  return map;
}

buttonmap_t& map = ButtonMap();

Controller::Controller() :
  A(0),
  B(1),
  X(2),
  Y(3) {
}

Controller::Button::Button(int type) :
  type_(type) {
  ButtonMap()[type] = this;
}

// main.cpp
#include "controller.h"
#include <iostream>
Controller controller;

int main(int argc, const char * argv[]) {
  std::cout << "running..." << std::endl;
  return 0;
}
person bandi    schedule 12.07.2013
comment
Не специфический для компилятора трюк, а гарантия языка. - person Sean Middleditch; 13.07.2013

Когда у вас есть несколько глобальных конструкторов, порядок их выполнения не определен. Если объект controller создается до объекта map, контроллер не сможет получить доступ к этой карте, что приводит к ошибке segfault. Однако, если сначала создается объект map, тогда контроллер может получить к нему доступ.

В этом случае, похоже, это порядок, в котором они появляются в командной строке. Вот почему размещение вашего main.cpp первым вызвало segfault - объект controller создавался первым.

Я бы рекомендовал переместить экземпляр в ваш main, потому что тогда вы можете точно контролировать, как создаются экземпляры и в каком порядке.

person Drew McGowen    schedule 12.07.2013
comment
+1. Эта проблема также известна как фиаско статического порядка инициализации. В C ++ faq lite есть несколько записей об этом. - person jrok; 13.07.2013
comment
Ваше решение - это то, как я изначально диагностировал то, что на самом деле происходило. Тем не менее, в идеале я все же хотел бы иметь весь код, связанный с Controller и Button, в отдельных файлах controller. *. Я просто обновил вопрос, чтобы указать на это. - person camall3n; 13.07.2013
comment
В этом случае вы можете просто добавить static функции к своим классам, которые создают экземпляры любых объектов, которые вам нужны, и вы можете вызывать эти функции из своего main. - person Drew McGowen; 13.07.2013

Как предложили @Drew McGowen и @jrok, я проверил "фиаско с порядком статической инициализации":
http://www.parashift.com/c++-faq/static-init-order.html
http://www.parashift.com/c++-faq/static-init-order-on-first-use.html

Используя идиому «построить при первом использовании», я внес следующие изменения:


controller.cpp

typedef std::map<int, Controller::Button*> buttonmap_t;
static buttonmap_t& map() // was buttonmap_t map;
{
    static buttonmap_t* ans = new buttonmap_t();
    return *ans;
};

//[...]

Controller::Button::Button(int type) :
type(type)
{
    map()[type] = this;
}


Это работает в соответствии с порядком компиляции и не требует никаких изменений в «controller.h» или «main.cpp».

person camall3n    schedule 12.07.2013