dlopen, фабричный шаблон и таблица виртуальных методов
Я пытаюсь обдумать, как работает фабричный шаблон при использовании dlopen в C++. Извините за длинный пост.
ТЛ; др; Вопрос выделен жирным шрифтом ниже.
Фрагменты из http://www.tldp.org/HOWTO/C++-dlopen/thesolution.html с удаленной проверкой ошибок для экономии места:
main.cpp
#include "polygon.hpp"
#include <iostream>
#include <dlfcn.h>
int main()
{
using std::cout;
// load the triangle library
void* triangle = dlopen("./triangle.so", RTLD_LAZY);
// load the symbols
// create function pointers
// (Exposed with extern "C")
polygon* create_triangle = (polygon*) dlsym(triangle, "create");
void* destroy_triangle = (void*) dlsym(triangle, "destroy");
// create an instance of the class
polygon* poly = create_triangle();
// use the class
poly->set_side_length(7);
cout << "The area is: " << poly->area() << '\n';
destroy_triangle(poly); // destroy the class
dlclose(triangle); // unload the triangle library
}
polygon.hpp
#ifndef POLYGON_HPP
#define POLYGON_HPP
class polygon
{
protected:
double side_length_;
public:
polygon()
: side_length_(0) {}
virtual ~polygon() {}
void set_side_length(double side_length)
{
side_length_ = side_length;
}
virtual double area() const = 0;
};
// the types of the class factories
typedef polygon* create_t();
typedef void destroy_t(polygon*);
#endif
triangle.hpp
#include "polygon.hpp"
#include <cmath>
class triangle : public polygon
{
public:
virtual double area() const
{
return side_length_ * side_length_ * sqrt(3) / 2;
}
};
// the class factories
extern "C" polygon* create()
{
return new triangle;
}
extern "C" void destroy(polygon* p)
{
delete p;
}
Поэтому, глядя на функцию main(), я вижу следующее.
- Длопен создает ручку.
- Указатели на функции созданы так, чтобы класс треугольника мог создать новый объект треугольника и уничтожить его. (Dlopen дает нам место памяти для перехода к.)
- create_triangle () возвращает треугольник, приведенный в многоугольник (так как мы знаем методы многоугольника).
- Мы устанавливаем внутренний член side_length_, используя метод set_side_length базового класса.
Вот вопрос:
Когда вызывается poly->area(), как это найти в объекте треугольника?
- Мы знаем, где в памяти у базового класса есть метод "virtual area()".
- Поскольку "triangle.so" загружается динамически, компилятор не может сказать, что я узнал, что область треугольника () переопределяет область полигона () в вашей программе.
- На этом этапе имена полностью искажены, и.so можно было бы скомпилировать с помощью clang++, а программу - с помощью g++. Таким образом, потенциально не имея надежды узнать их на данный момент.
Сохраняет ли таблица виртуальных членов виртуальные методы в порядке в соответствии с тем, когда они появляются? Так не нарушит ли этот код?
polygon.hpp
...
virtual double area() const = 0;
virtual double parameter() const = 0;
...
triangle.hpp
...
double parameter() const { ... } // implementing and defining parameter first
area() const { ... } // implementing and defining second.
...
Я знаю, что вы хотели бы держать их в порядке... но допустим, мы подкласс еще пару раз, и они определены в другом порядке...
Любая помощь в этом была бы отличной. Я просто не могу представить, что здесь происходит в памяти, чтобы это на самом деле работало.
Спасибо! И извините за длинный пост.
2 ответа
Когда вызывается poly->area(), как это найти в объекте треугольника?
Вся инициализация poly
происходит внутри библиотеки (включая настройку vptr). Единственное, что должен сделать вызывающий (то есть исполняемый файл), это загрузить указатель виртуального метода из poly
vtable по определенному индексу. И исполняемый файл, и shlib разделяют объявление для poly
класс, так что они оба согласны, какой элемент vtable соответствует area
,
Обратите внимание, что компилятору не нужно ничего знать о том, как реализация poly
перегружает базовый класс.
.so можно было скомпилировать с помощью clang++, а программу можно скомпилировать с помощью g ++
Clang будет очень трудно быть совместимым с ABI с GCC на платформах Linux (с Visual Studio на платформах Windows) для достижения такого типа взаимодействия.
Я знаю, что вы хотели бы держать их в порядке... но допустим, мы подкласс еще пару раз, и они определены в другом порядке...
Подклассы не изменили бы структуру базового класса vtable, которая исправлена в этой точке (иначе полиморфизм также сломался бы).
Обратите внимание, что для повышения совместимости между компиляторами (а также безопасности), я бы рекомендовал сделать интерфейс (polygon
) Деструктор защищен и не виртуален.
Почему защищены
В общем случае DLL не обязательно должна иметь ту же кучу, что и приложение, использующее ее (это особенно актуально при использовании между различными компиляторами), поэтому вы должны запретить вызов delete
на интерфейсе (именно поэтому вы предоставляете destroy()
функция).
Почему не виртуальный
Это просто по технической причине, потому что виртуальный деструктор часто реализуется по-разному в разных компиляторах. Одним из примеров является несовместимость между MSVC и MinGW (GCC) под Windows - MSVC создает одну запись виртуальной таблицы для виртуального деструктора, тогда как GCC создает для нее 2 записи. Что делает виртуальную таблицу смещенной на один элемент. С (встроенным тривиальным) не виртуальным деструктором нет записи в виртуальной таблице для деструктора.
Кроме того, вместо предоставления дополнительного destroy()
функция, я обычно предоставляю чисто виртуальную destroy()
метод в самом интерфейсе (с помощью простого вызова delete this;
). Необязательно с встроенной оболочкой, которая проверяет, является ли указатель интерфейса не NULL (потому что очевидно, что вызов виртуального метода по указателю NULL не будет работать). И в лучшем случае опционально обернут в умный указатель, который на самом деле заботится о вызове obj->destroy()
,