Comments 25
Объект класса А занимает 4 байта (поле типа int), независимо от сложности предоставляемого интерфейса.
В вашей реализации объект класса А занимает:
16 байт * 2 (количество_методов в интерфейсе) +
4 байта для указателя на объект (unique_ptr) +
4 байта A_context +
8 байт для указателя на контекст (shader_ptr) +
8 байт объект bind +
8 байт для указатель на контект в bind'е +
8 байт для указатель на контект в лямбде.
А если в интерфейсе 10 методов?
В вашей статье речь идет не об изменении заголовочного файла, а об изменение закрытых полей некоторого класса. Если в вашем проекте есть класс, в котором часто меняются приватные поля и это класс цепляется по всему большому проету (но он должен цепляться в явном виде, чтобы приводить к перекомпеляции) — то что-то в архитектуре этого проекта в принципе не так.
И потом, из стать несовсем понятно, чем ваш пример лучше pimpl? Вы привели единственный аргумент
необходимость писать обертку для всех методов класса примерно таким образом (опустим дополнительные сложности по управлению памятью):
Как заметели в коментариях ниже, такой необходимости нет.
Например, в качестве скрытой структуры может выступать A_context из вашего примера, тогда можно реализовать методы естественным путем, без дополнительного уровня косвенности.
По поводу памяти, можно также использовать unique_ptr для хранения указателя на реализацию. Плюс ко всему здесь есть улучшение под название The Fast Pimpl Idiom.
// A.h
class A
{
public:
A();
~A();
void next_step();
int result_by_module(int);
private:
struct impl;
std::unique_ptr<impl> _impl;
};
// A.cpp
struct A::impl
{
int counter_ = 0;
};
A::A()
: _impl(std::make_unique<impl>())
{}
A::~A() = default;
void A::next_step()
{
++_impl->_counter;
}
int A::result_by_module(int m)
{
return _impl->_counter % m;
}
Также, на мой взгляд, никаким улучшением времени компеляции, путь даже она будет происходит мгновенно при любом изменение, нельзя оправдать увеличение потребления памяти в рантайме с учетом того, что производительность останется таже. Вы же этот продукт в конечном итоге отдатите пользователям. Как минимум, все подобные манипуляции должы производиться только под дебагом. И здесь pimpl опять лучше, потому что не меняет интерфейс класса. Если я без скрытой реализации писал a.result_by_module(4), то и со скрытой реализацией я буду писать а.result_by_module(4).
Еще ваша реализация полность убирает понятие константности из интерфейса.
В вашем примере есть явное нарушение инкапсуляции, можно сделать так:
a->_result_by_module = std::function<int(int)>();
Хотя этого можно избежать.
есть недостаток: необходимость писать обертку для всех методов класса
Зачем? По умолчанию никакой необходимости в этом нет.
Думаю у такого подхода действительно есть много преимуществ, но не совсем тех, которые описаны в статье.
Вариант любопытный. Но к сожалению такой же костыль для хождения по граблям как и все остальные. Он не решает гораздо более пакостную проблему — namespace pollution, когда вместе с безобидным заголовком вектора в пространство видимости попадает половина стандартной библиотеки.
Модули, которых нет. И не будет минимум до 20-го года. А то и дольше, учитывая скорость работы комитета.
Проблема в том, что это решение реально получается толще pimpl и больше подвержено всяким фокусам вроде переназначения делегатов.
Т.е., если нужно добавить метод, то при PIMPL нужно: 1. поправить объявление реализации, 2. поправить определение реализации 3. Поправить определение обертки 4. поправить определение обертки.
В описанном подходе нужно: 1. добавить поле-функтор, 2. добавить лямбду / бинд в «конструкторе», 3. (если использован бинд) сделать свободную функцию с реализацией.
Да бывает такое, что лезешь менять незначительную вещь, а компилируется весь проект.
Но!
На каком этапе предлагается это городить? На этапе проектирования? То есть когда модуль еще не написан?
На этапе проектирования, и написания модуля таких проблем не стоит. Потому, что этот модуль мы пишем и тестируем до того как инклюдить его.
На этапе готового рабочего проекта когда нужно залезть в основополагающий класс?
Такие задачи возникают и да действительно долго компилировать проект. Но стоит ли оно того чтобы его переписывать так так вот?
Ну может быть пригодиться.
Если уж захотелось делать интерфейсы, то зачем громоздить, ведь что PIMPL, что NVI требуют изобретательного рукоделия. Тогда как штатный способ в ООП — на старых добрых виртуальных функциях.
А что мешает по старинке отделить интерфейс?
// A.h
#pragma once
struct A {
static A* create();
virtual ~A() {}
virtual void next_step()=0;
virtual int result_by_module(int m)=0;
};
// A.cpp
#include "A.h"
struct A1 : A {
int counter;
A1() { counter=0; }
int result_by_module(int m) { return _counter % m; }
void next_step() { ++counter; }
};
A* A::create() { return new A1(); }
Предусловие: 1) неудобно/ненужно (править неудобно, кода получается сильно больше) 2) performance penalty (на виртуальные вызовы) при выделении интерфейсов для всех публичных классов. Далее, если необходимо добавить в публичный класс поле с приватным объектом - приватный объект приходится тащить в публичный заголовок. Изложенный подход лишен недостатка необходимости тянуть все кишки публичного класса в общую область видимости. Ну и надо воспринимать изложенное как некоторую гиперболизацию.
неудобно/ненужно (править неудобно, кода получается сильно больше)спорно, с учётом дублирования в сигнатур в структуре-интерфейсе, статических функциях и необходимости ручной поддержки портянок std::bind в create_A
performance penalty (на виртуальные вызовы) при выделении интерфейсов для всех публичных классовРовно как и у вас — вызов через указатель, который std::function сохранит в свои внутренности при вызове bind. Только виртуальные ф-ции вылизывались компиляторами десятки лет, и если (как в примере выше) у интерфейса A ровно одна реализация A1, компилятор сделает девиртуализацию и вызовы сделает прямыми. Чего не сможет сделать с вашими std::function, потому что просто не знает ничего про этот паттерн.
А ещё представьте, что у вас в A не 2 метода, а 100. Тогда create_A выделит 100 функций, в каждой из которых будет храниться по 2 указателя — на static реализацию, и на context. «Толщина» объекта линейно зависит от числа функций его интерфейса, когда в pImpl, всегда 1 указатель — на класс-реализацию, в котором 1 указатель на таблицу вирт. методов (единственную на всю программу), и этот же класс является контекстом, т.е. указатель контекста не нужен.
Далее, если необходимо добавить в публичный класс поле с приватным объектом — приватный объект приходится тащить в публичный заголовокНет, смотрите в примере выше — добавляем приватный объект внутрь класса A1, который лежит в непубличном файле A.cpp
Тупо интерфейс (ABC)
class A
{
public:
A(){}
virtual ~A(){}
int func1(int) = 0;
void func2() = 0;
};
решает все заявленные проблемы с видимостью кишочков класса снаружи, временем перекомпиляции, открывает возможности для использования фейков-моков при тестировании. Если есть единственный наследующий класс, да еще использовать final, то компилятор может с большой долей вероятности девиртуализировать все виртуальные функции и тем самым избежать даже мизерного провала по производительности. А еще такой подход идиоматичен, понимается другими программистами на лету, и поддерживается любым нормальным IDE.
Разделяем интерфейс и реализацию в функциональном стиле на С++