Pull to refresh

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 в обертке получается достаточно много тривиальных методов, которые лишь делегируют управление.
Т.е., если нужно добавить метод, то при PIMPL нужно: 1. поправить объявление реализации, 2. поправить определение реализации 3. Поправить определение обертки 4. поправить определение обертки.
В описанном подходе нужно: 1. добавить поле-функтор, 2. добавить лямбду / бинд в «конструкторе», 3. (если использован бинд) сделать свободную функцию с реализацией.

Как-то многовато действий. Методы внешнего типа могут использовать данные из PImpl напрямую — и тогда накладные расходы будут в одном лишнем уровне индирекции.

Обычно это решается доступным снаружи интерфейсом и фабрикой, возвращающей shared_ptr.
Виртуальные вызовы. Пропадает возможность inline.
Впрочем, те же проблемы у pimpl, но для вашего решения меньше писать лишнего кода.
Ну мысль понятна. Интересная мысль, спасибо. Но мне не понятно какую проблему это решает?

Да бывает такое, что лезешь менять незначительную вещь, а компилируется весь проект.
Но!

На каком этапе предлагается это городить? На этапе проектирования? То есть когда модуль еще не написан?
На этапе проектирования, и написания модуля таких проблем не стоит. Потому, что этот модуль мы пишем и тестируем до того как инклюдить его.

На этапе готового рабочего проекта когда нужно залезть в основополагающий класс?
Такие задачи возникают и да действительно долго компилировать проект. Но стоит ли оно того чтобы его переписывать так так вот?
Ну может быть пригодиться.
Это — приём (не хочу говорить «паттерн») NVI — невиртуальный интерфейс.
Если уж захотелось делать интерфейсы, то зачем громоздить, ведь что 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 (на виртуальные вызовы) при выделении интерфейсов для всех публичных классов. Далее, если необходимо добавить в публичный класс поле с приватным объектом - приватный объект приходится тащить в публичный заголовок. Изложенный подход лишен недостатка необходимости тянуть все кишки публичного класса в общую область видимости. Ну и надо воспринимать изложенное как некоторую гиперболизацию.

Подход в статье — просто другой способ записи, синтаксический сахарок, если угодно, вокруг pImpl. С точки зрения оптимизаций ничего полезного он не несёт.

неудобно/ненужно (править неудобно, кода получается сильно больше)
спорно, с учётом дублирования в сигнатур в структуре-интерфейсе, статических функциях и необходимости ручной поддержки портянок 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
Это жесть какая-то — заворот кишок с неясно выраженным профитом. Вы бы хотели писать такой код? Не говоря уж о накладных расходах на память, процессор и на то, что компиляторы плохо умеют девиртуализировать std::function и std::bind

Тупо интерфейс (ABC)

class A
{
public:
A(){}
virtual ~A(){}
int func1(int) = 0;
void func2() = 0;
};


решает все заявленные проблемы с видимостью кишочков класса снаружи, временем перекомпиляции, открывает возможности для использования фейков-моков при тестировании. Если есть единственный наследующий класс, да еще использовать final, то компилятор может с большой долей вероятности девиртуализировать все виртуальные функции и тем самым избежать даже мизерного провала по производительности. А еще такой подход идиоматичен, понимается другими программистами на лету, и поддерживается любым нормальным IDE.
Sign up to leave a comment.

Articles