C++/Go/Python
Game Developer

Почему перестало компилироваться? Особенности dllexport

Имеется следующий код:

class MyTrickyClass
{
public:
    MyTrickyClass() = default;
    
    // public interface ... 
    
private:
    std::map<int, std::unique_ptr<SomeData>> myTrickyMap;
}

Самый внимательный уже понимает к чему движется дело.

Используется класс очень просто, для упрощения предпложим как-то так:

MyTrickyClass &GetTrickyClass()
{
    static MyTrickyClass instance;
    return instance;
}

(синглетоны - это плохо, не надо так)

Вышеописанный код замечательно компилируется и даже корректно работает. Но неожиданно понадобилось этот класс перенести в динамическую библиотеку. Код приобрел следующий вид:

#ifdef MY_TRICKY_EXPORTS
    #define MY_TRICKY_API __declspec(dllexport)
#else
    #define MY_TRICKY_API __declspec(dllimport)
#endif

class MY_TRICKY_API MyTrickyClass
{
public:
    MyTrickyClass() = default;
    
    // public interface ... 
    
private:
    std::map<int, std::unique_ptr<SomeData>> myTrickyMap;
}

Все уже поняли, что говорим под Windows? :)

Библиотека собирается с дефайном MY_TRICKY_EXPORTS, поэтому используется аттрибут __declspec(dllexport), а код использующий этот класс __declspec(dllimport). Это все нужно для указания линкеру как правильно линковать и где искать символы.

Компилируем… и получаем следующую ошибку:

C2280 'std::pair::pair(const std::pair &)': attempting to reference a deleted function

Ага, компилятор пытается сгенерировать конструктор копирования пары. Хм, зачем? Ему нужно сгенерировать конструктор копирования MyTrickyClass, и, соответственно, конструктор копирования мапы, который использует этот конструктор. Но почему использование директивы __declspec(dllexport) заставило компилятор генерировать MyTrickyClass(const MyTrickyClass &other)?

Оказывается, это даже логично. Обычно, компилятор знает какие дефолтные конструкторы и функции класса используются. Когда класс не экспортировался, компилятору нет нужды генерировать конструктор копирования, так как он явно нигде не используется. А вот если мы начинаем экспорировать, компилятор не знает ЧТО может использоваться в другом коде, поэтому генерирует все дефолтные методы, в том числе и конструктор копирования и оператор присваивания.

Как сообщить компилятору, что эти методы не нужны? И даже запрещены, так как у std::unique_ptr конструктор копирования и оператор присваивания удалены. Все просто - так же удалим дефолтные методы (говорим, конечно же, про C++11):

class MyTrickyClass
{
public:
    MyTrickyClass() = default;

    MyTrickyClass(const MyTrickyClass &other) = delete;
    MyTrickyClass& operator=(const MyTrickyClass&) = delete;
    
    // public interface ... 
    
private:
    std::map<int, std::unique_ptr<SomeData>> myTrickyMap;
}

Profit!