Pull to refresh

Comments 29

А как бы ещё эту задачу решил функциональный подход?

Можно принимать тип-сумму в функции. В случае C++ std::variant<ParserJsonImpl, ParserXmlImpl>

я вполне возможно и не прав, но вероятно имелось в виду подобное:


data Parser = XMLParser | JSONParser
data ParsingResult = Data Int Int | Error

parseFile :: String -> Parser -> ParsingResult
parseFile str XMLParser = -- ...
parseFile str JSONParser = -- ...

Вы правы, вот вариант на C++


    struct ParserJsonImpl {
        int getData() {
            std::cout << "ParserJsonImpl::getData()\n";
            return 0;
        }

        int getID() {
            std::cout << "ParserJsonImpl::getID()\n";
            return 0;
        }
    };

    struct ParserXmlImpl {
        int getData() {
            std::cout << "ParserXmlImpl::getData()\n";
            return 0;
        }

        int getID() {
            std::cout << "ParserXmlImpl::getID()\n";
            return 0;
        }
    };

    using parser_t = std::variant<ParserJsonImpl, ParserXmlImpl>;

    std::pair<int, int> parseFile(parser_t parser) {
        auto visitor = [](auto parserImpl) {
            return std::make_pair(parserImpl.getData(), parserImpl.getID());
        };
        return std::visit(visitor, parser);
    }
И зачем здесь CRTP?
template <typename Implementation>
class ParserInterface : Implementation {
public:
    int getData() {
        return this->getDataImpl();
    }
};

struct ParserJsonImpl {
    int getDataImpl() {
        std::cout << "ParserJsonImpl::getData()\n";
        return 0;
    }
};

using JsonParser = ParserInterface<ParserJsonImpl>;

int main() {
    JsonParser jsonParser;
    jsonParser.getData();

    return 0;
}

вот если бы вы из имплементации в родителя ходили — тогда да
проблема CRTP не в шаблонах, а в том, что мы получаем двунаправленную конструкцию интерфейс <-> реализация. В навигации по такому коду IDE не поможет, читать «вручную» тоже проблемно. В итоге бонус в производительности может быть перевешен банально читаемостью решения на виртуальных методах.

QtCreator с clang бэкэндом вполне себе подсвечивает и навигацию делает
Вообще CRTP нужен не для композиции а для абстракции, а там проблем с подсветкой быть не должно — сигнатуры объявлены явно

дело не в подсветке, а в навигации по коду. Грубо говоря, для такого сниппета:
template <class T>
class Base {
public:
    void foo() {
         static_cast<T*>(this)->bar();
    }
};
IDE не сможет перейти к определению bar() в наследнике. А теперь представьте на секунду что у вас не такой простой сценарий, а, как часто бывает, несколько уровней/ветвлений наследования, несколько классов реализаций, и еще и код структурирован через одно место потому что шаблоны не очень располагают к out of line определениям. Приперчим это всем синтаксическим мусором, который необходим CRTP, и в типовом случае мы получаем код, который способен прочитать дай бог хотя бы его автор.

А теперь представьте что вам нужно разбираться в этом, а автор давным давно уволился. Вот за это я CRTP и не люблю

Приведенный сниппет это не CRTP и вообще за такое по рукам бить надо :)
Но я согласен, что шаблонное наследование IDE не может подсказать правильно — в тяжелых случаях для отладки я задаю умолчание для шаблона чтобы IDE сумела понять что там происходит. Но отладку никто не отменял, так что компилируем и смотрим на ошибки, а если не выходит то расчехляем cling.

Приведенный сниппет это не CRTP и вообще за такое по рукам бить надо :)
как только мы от этого Base наследуемся, вот так:
class Derived : public Base<Derived> { ... }

получается самый что ни на есть CRTP
Но отладку никто не отменял, так что компилируем и смотрим на ошибки, а если не выходит то расчехляем cling.
смотреть на ошибки компилятора в коде с кучей шаблонов, говорите?
Я имел ввиду что CRTP там бесполезен, почему — написал в другой ветке.
Это всё равно что для объяснения виртуальных методов не использовать наследование — вроде как и есть, а по сути — нет.
смотреть на ошибки компилятора в коде с кучей шаблонов, говорите?

Ну да, самый простой, эффективный и одновременно недооценённый способ — смотреть что тебе выводит компилятор. В это трудно поверить, но учёные выяснили что вывод компилятора по сути представляет из себя диагностику ошибки с полным описанием рассмотренных вариантов разворачивания шаблонов и указанием на то почему конкретный вариант не может быть использован. Тайное знание как это делать — читать и исправлять только первую ошибку.
В это трудно поверить, но учёные выяснили что вывод компилятора по сути представляет из себя диагностику ошибки с полным описанием рассмотренных вариантов разворачивания шаблонов и указанием на то почему конкретный вариант не может быть использован
поймите меня правильно, в обычном случае ошибки компилятора с++ вполне читаемые. С некоторым количеством шаблонов тоже, хоть и менее приятно. А вот когда у вас код обложен кучей шаблонов, в особенности c CRTP, при этом классы еще и из разных пространств имен, весь этот шум начинает топить информативную часть сообщения. У меня были ситуации когда ошибка компиляции разворачивалась на 100+ сообщений по абзацу каждое.
Тайное знание как это делать — читать и исправлять только первую ошибку.
на моей практике последнее сообщение в стеке обычно более полезное.
В классе ParserInterface необходимо добавить определение функций getDataImpl() и getIDImpl() по умолчанию, на случай если в классе-наследнике одна из них не нужна. Сейчас, если в наследнике «забыть» определить любую из них, будет ошибка компиляции. А так код сохранит работоспособность, просто при вызове из объекта базового класса ParserInterface, параметризованого классом с отсутствующим методом, будет вызвана «заглушка» определенная в базовом классе. Аналог виртуальной функции.

В общем случае это является одной из фишек CRTP подхода — проверка интерфейса наследуемого класса на этапе компиляции.
Я бы использовал вместо слова "необходимо" слово "возможно".

Ещё лучше пометить как delete
В общем случае это является одной из фишек CRTP подхода — проверка интерфейса наследуемого класса на этапе компиляции.
с виртуальными функциями тоже методы на этапе компиляции проверяются…

Да — проверяется, но при условии использования ключевого слова override.


Допустим, что используемый компилятор не поддерживает override. Тогда при любой синтаксической ошибке в объявлении метода мы получаем код, который прекрасно компилируется, но не работает так как от него ожидается. Заметить такую опечатку не просто.

чтобы код скомпилировался, все чистые виртуальные методы должны быть определены, тут особой разницы с CRTP нет. override проверяет то, что метод действительно перегружает базовый, такого аналога у CRTP нет. А доступен он начиная с с++11, что в общем-то практически везде…

Ах если бы override был везде...


А вот с чистыми виртуальными методами согласен полностью. Более того сам использую в практике. Причем, делаю их protected и публичный метод для вызова. Такой прием используется для разделения определения интерфейса (публичные методы в базовом классе) и реализации (virtual protected методы в наследниках).
В такой связке и override не очень нужен.


Но если есть возможность (читай потребность) использовать CRTP — использую всегда.

Но если есть возможность (читай потребность) использовать CRTP — использую всегда.
когда дело доходит до CRTP, в подавляющем большинстве случаев есть возможность его использования, но нет потребности. И размен читаемости на перф с ним не шибко выгодный

Читаемость или не читаемость — это слишком индивидуальная оценка…
а вот оценка быстродействия и экономия памяти — это вполне себе измеримая характеристика.


Просто надо понимать что даёт CRTP и трезво взвешивать выгоду, которую можно от него получить.


В моей практике даже начинающие программисты довольно быстро вникали в суть CRTP и начинали его использовать (иногда не там, где нужно).

Читаемость или не читаемость — это слишком индивидуальная оценка…
а вот оценка быстродействия и экономия памяти — это вполне себе измеримая характеристика.
время, которое необходимо сотруднику для внесения изменения в код, сложно замерить, но можно и оценить. Быстродействие/потребление тоже можно замерить, но подумайте сами — тратили бы вы пару часов на оптимизацию, которая не сэкономит эту самую пару часов времени железа за всё суммарное время эксплуатации софтины?
Просто надо понимать что даёт CRTP и трезво взвешивать выгоду, которую можно от него получить.
о чем я и говорю — многие сильно недооценивают влияние CRTP на код и сильно переоценивают его влияние на перф.
В моей практике даже начинающие программисты довольно быстро вникали в суть CRTP и начинали его использовать (иногда не там, где нужно).
проблема не в том, что абстрактный начинающий программист Вася напишет код с использованием CRTP. Проблема в том, что абстрактный программист Петя уже не сможет разобраться, что там написал Вася, и перепишет с нуля. И если Петя тоже начинающий, мы получаем цикл.
тратили бы вы пару часов на оптимизацию, которая не сэкономит эту самую пару часов времени железа за всё суммарное время эксплуатации софтины?

При такой постановке вопроса — ответ нет.


многие сильно недооценивают влияние CRTP на код и сильно переоценивают его влияние на перф.

О чем я и говорю — "трезво взвешивать выгоду"


Проблема в том, что абстрактный программист Петя уже не сможет разобраться, что там написал Вася, и перепишет с нуля. И если Петя тоже начинающий, мы получаем цикл.

Мне уже надоел аргумент о не квалифицированном программисте (специалисте, технике, сантехнике, электрике и так далее). Этот аргумент сродни переходу на личности. Я бы не хотел вести дискуссию в таком русле.


Программирование требует самоотдачи и постоянного повышения квалификации (как и любой другой вид деятельности впрочем) и постоянно говорить и неком "Васе" или "Пете", которые не разобравшись перепишут с нуля как минимум не логично. Как минимум потому, что порог входа в программирование существенно выше и требует определенного склада ума.


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


Мой изначальный посыл был лишь об одном свойстве CRTP — проверке интерфейса наследуемого класса на этапе компиляции. Как по мне, так это однозначный плюс.

Мне уже надоел аргумент о не квалифицированном программисте
мы всё еще говорим о читаемости кода, верно? Если код на виртуальных функциях может эффективно прочитать плюсовик с 1-2 годами опыта коммерческой разработки, то CRTP аналог такого кода потребует коллегу с 5+ годами опыта.
Мой изначальный посыл был лишь об одном свойстве CRTP — проверке интерфейса наследуемого класса на этапе компиляции. Как по мне, так это однозначный плюс.
так виртуальные методы тоже проверяются на этапе компиляции
то CRTP аналог такого кода потребует коллегу с 5+ годами опыта.

Мой опыт говорит о другом.


так виртуальные методы тоже проверяются на этапе компиляции

Так мы с этого и начали.


Чтобы виртуальные методы проверялись, нужно либо использовать чистые виртуальные методы, либо использовать ключевое слово override.


CRTP не требует ни того, ни другого.


P.S. Это не призыв использовать CRTP вместо виртуальных методов. Это просто констатация факта.

Допустим, что используемый компилятор не поддерживает override.

можно только посочувствовать :(

Окончательный код путает.


    ParserJsonImpl jsonParser;
    parseFile(jsonParser);

    ParserXmlImpl xmlParser;
    parseFile(xmlParser);

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

 parseFile(ParserInterface<Impl> parser)

что-то тут не так. Или специально сделано не константной ссылкой?
Sign up to leave a comment.

Articles