LINUX.ORG.RU

Наркоманский Pimpl

 ,


0

3

Те, кто хоть сколько-нибудь программировал на C++ знают, как бесит ситуация, когда приватные детали класса тянут за собой кучу ненужного для пользователя класса барахла:

#include <junk1.h>
#include <junk2.h>
class Foo {
 public:
  static std::unique_ptr<Foo> Make();
  void DoWork();
 private:
  Foo();
  Junk1 left_hand;
  Junk2 right_hand;
};

Пользователь работает с классом только через Make и DoWork, но из-за того, что нельзя сделать forward declaration отдельно для методов – приходится ребилдить свой код при каждом изменении junk1 и junk2.

Классические способ решения проблемы:

  1. Популярный Pimpl – спрятать Junk1 и Junk2 в другой класс, а в хедере оставить указатель на forward declaration. Минусы: ручной this, двойное разыменование;
  2. Сделать DoWork виртуальным, а в фабрике возвращать наследника. Минусы: неявный вызов, много boilerplate.
  3. Аналогично виртуальным методам, но кастить Foo* на FooImpl*. Опять boilerplate, но быстро.

Но упарываясь очередной ночью без сна мне пришла иная мысль. Ведь по сути мы хотим сделать forward declaration функции с неявным this, но синтаксис нам этого не позволяет. Но что если взять расширение компилятора, и разрешить проблему на уровне линковщика? Вуаля:

#include <stdio.h>

struct Foo {
    int add(int a, int b) asm("_ZN7FooImpl3addEii");
protected:
    Foo() noexcept = default;
};

struct FooImpl : Foo {
    using Foo::Foo;
    int add(int a, int b);
    int x;
};

int FooImpl::add(int a, int b) {
    printf("%s: Adding %d with %d:\t Result:%d\n", 
            __PRETTY_FUNCTION__, a, b, a+b+x);
    return a+b+x;
}

int main()
{
    FooImpl obj;
    obj.x = 10;
    obj.add(3, 6);

    Foo &erased = obj;
    erased.add(3, 5);
    return 0;
}

Код работает на всех версиях gcc, clang, icc. Для GCC также работает __attribute__ ((alias ("_ZN7FooImpl3addEii"))).

Как думаете, был бы полезен плагин gcc/clang, реализующий атрибут на уровне класса, чтобы для undefined symbols автоматически делать переименования символа?

★★★★★

Как Вы собираетесь бороться с тем фактом что даже в этом простеньком примерчике sizeof(Foo) != sizeof(FooImpl)?

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

bugfixer ★★★★★
()

приходится ребилдить свой код при каждом изменении junk1 и junk2

что, это прямо такая большая проблема? Если напихать в код всяких хаков на ассемблере, то ребилдить придется не меньше, а гораздо больше.

В целом, жертвовать рантаймом ради облегчения разработки - это очень плохая идея. Зачем тогда вообще на C++ писать, когда есть всякие го, питоны и т.п.?

return 0 не нужен в конце main…

Lrrr ★★★★★
()
Последнее исправление: Lrrr (всего исправлений: 1)

Популярный Pimpl – спрятать Junk1 и Junk2 в другой класс, а в хедере оставить указатель на forward declaration. Минусы: ручной this, двойное разыменование;

Ручной this - это что именно в данном случае?

rumgot ★★★★★
()

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

вот как только ваши хаки нарушат размеры и смещения - все упадет.

теоретически приватные невиртуальные методы можно не публиковать(то есть сделать язык со скрытым продолжением класса где либо), но в плюсах принято все методы показывать в определении класса. скорее всего из-за принципа - «полной декларации класса в одном месте»

alysnix ★★★
()

в вашем решении попытка отнаследоваться от Foo еще в одном классе закончится плачевно, поскольку ему хакнули add, а размер его таков, что там нет поля x. а x нужен хакнутой add.

то есть прямо на извилистом пути погромиста расположена огромная грабля, на которую неминуемо наступят. и все кончится сегфолтом.

alysnix ★★★
()

А ещё при написании юнит-тестов наверняка возникнет потребность вместо FooImpl инджектить FooMock. В варианте с базовым классом-интерфейсом IFoo это делается красиво и чисто, а с варианте с pimpl - криво и грязно.

Manhunt ★★★★★
()

при решении данной проблемы можно воспользоваться иным способом теоретического ононизма, годным для с++.

а именно - определить в базовом Foo массив байтов подходящего размера, куда и класть потом скрытые поля методом инплейс конструирования. тогда поля будут невидны снаружи, но видны «изнутри», а также наследникам.

тут будет все чисто и «большой грабли» не будет, то есть можно наследоваться снаружи и все смещения считаются.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 2)

Короче, я расскажу тебе фишку.

приходится ребилдить свой код при каждом изменении junk1 и junk2.

Для этого в плюсах сделали модули. Пользуйся ими!

hateyoufeel ★★★★★
()

Итого,

@bugfixer @Lrrr @alysnix не поняли суть, но потом @alysnix предложил аналог, который я знал, но было лень писать. Он тоже генерирует boilerplate на ровном месте.

@hateyoufeel модули, конечно, решают проблему на корню, но не уверен, что они готовы к продакшену. И требования к версии компилятора сильно поднимают.

Я тут поспал и подумал, что вместо извращений с mangle можно просто аккуратным жонглированием ifdef-ами сделать разные типы: один без деталей для хедеров, а второй для сборочного юнита.

snizovtsev ★★★★★
() автор топика
Ответ на: комментарий от snizovtsev

вместо извращений с mangle можно просто аккуратным жонглированием ifdef-ами сделать разные типы: один без деталей для хедеров, а второй для сборочного юнита.

Что является прямым нарушением ODR.

bugfixer ★★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от alysnix

Пользователь работает с классом только через Make и DoWork,

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

то есть ты изобрел какой-то костыль для своего частного случая.

тебя на пустом месте парит, что нельзя написать obj->add(10,20)? тогда напиши функцию add(this_ptr, int, int ). а она уже внутри будет делать правильный add. С формальной точки зрения ты сделал ровно это, только через Ж. приделав asm и мэнглированное имя, которое еще надо найти.

alysnix ★★★
()
Ответ на: комментарий от snizovtsev

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

К продакшену они не готовы, потому что перцы из LLVM/GNU дрочат и никак не могут libstdc++ в модули обернуть. В MSVS вроде готово всё.

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

hateyoufeel ★★★★★
()
Ответ на: комментарий от bugfixer

Что является прямым нарушением ODR.

Это верно:

Note: in C, there is no program-wide ODR for types, and even extern declarations of the same variable in different translation units may have different types as long as they are compatible. In C++, the source-code tokens used in declarations of the same type must be the same as described above: if one .cpp file defines struct S { int x; }; and the other .cpp file defines struct S { int y; };, the behavior of the program that links them together is undefined.

Но думаю забить. Вряд ли выстрелит. Хотя, теоретически, могут быть проблемы с LTO.

snizovtsev ★★★★★
() автор топика
Ответ на: комментарий от red75prim

А я всегда пишу. Не люблю больно-ублюдочные исключения из правил.

Это не исключение, это - обратная совместимость с K&R C года так 1975-го.

Нахрена? Код из 1975го не скомпилируется современным компилятором и наоборот.

hateyoufeel ★★★★★
()