… как наполнить шаблонный класс разным содержимым в зависимости от значений параметров шаблона? Когда-то, уже довольно давно, язык D начали делать как "правильный C++" с учетом накопившегося в C++ опыта. Со временем D стал не менее сложным и более выразительным языком, чем C++. И уже C++ стал подсматривать за D. Например, появившийся в C++17 if constexpr, на мой взгляд, — это прямое заимствование из D, прототипом которому послужил D-шный static if. К моему сожалению, if constexpr в С++ не обладает такой же мощью, как static if в D. Тому есть свои причины, но все-таки бывают случаи, когда остается только пожалеть, что if constexpr в C++ не позволяет управлять наполнением C++ного класса. Об одном из таких случаев и хочется поговорить. Речь пойдет о том, как сделать шаблонный класс, содержимое которого (т.е. состав методов и логика работы некоторых из методов) менялось бы в зависимости от того, какие параметры были переданы этому шаблонному классу. Пример взят из реальной жизни, из опыта разработки новой версии SObjectizer-а. Требуется создать хитрый вариант "умного указателя" для хранения объектов-сообщений. Чтобы можно было написать что-то вроде: Хитрость этого класса message_holder_t в том, что нужно учесть три важных фактора. Типы сообщений, которыми параметризуется message_holder_t, делятся на две группы. Первая группа — это сообщения, которые наследуются от специального базового типа message_t. Например: В этом случае message_holder_t внутри себя должен содержать только указатель на объект этого типа. Этот же указатель должен возвращаться в методах-getter-ах. Т.е., для случая наследника от message_t должно быть что-то вроде: Вторая группа — это сообщения произвольных пользовательских типов, которые не наследуются от message_t. Например: Экземпляры таких типов в SObjectizer-е отсылаются не сами по себе, а заключенными в специальную обертку user_type_message_t<M>, которая уже наследуется от message_t. Поэтому для таких типов message_holder_t должен содержать внутри себя указатель на user_type_message_t<M>, а методы-getter-ы должны возвращать указатель на M: Второй фактор — это деление сообщений на неизменяемые (immutable) и изменяемые (mutable). Если сообщение неизменяемое (а по умолчанию оно неизменяемое), то методы-getter-ы должны возвращать константный указатель на сообщение. А если изменяемое, то getter-ы должны возвращать не константный указатель. Т.е. должно быть что-то вроде: Третий фактор — это логика поведения message_holder_t как умного указателя. Когда-то он должен вести себя как std::shared_ptr, т.е. можно иметь несколько message_holder-ов, ссылающихся на один и тот же экземпляр сообщения. А когда-то он должен вести себя как std::unique_ptr, т.е. только один экземпляр message_holder-а может ссылаться на экземпляр сообщения. По умолчанию, поведение message_holder_t должно зависеть от изменяемости/неизменяемости сообщения. Т.е. с неизменяемыми сообщениями message_holder_t должен вести себя как std::shared_ptr, а с изменяемыми, как std::unique_ptr: Но жизнь штука сложная, поэтому нужно иметь еще и возможность вручную задать поведение message_holder_t. Чтобы можно было сделать message_holder-а для иммутабельного сообщения, который ведет себя как unique_ptr. И чтобы можно было сделать message_holder-а для изменяемого сообщения, который ведет себя как shared_ptr: Соответственно, когда message_holder_t работает как shared_ptr, у него должен быть обычный набор конструкторов и операторов присваивания: и копирования, и перемещения. Кроме того, должен быть константный метод make_reference, который возвращает копию хранящегося внутри message_holder_t указателя. А вот когда message_holder_t работает как unique_ptr, то конструктор и оператор копирования у него должны быть запрещены. А метод make_reference должен изымать указатель у объекта message_holder_t: после вызова make_reference исходный message_holder_t должен остаться пустым. Итак, нужно создать шаблонный класс: у которого: Последние два пункта из перечня определяются параметром Ownership (а также мутабельностью сообщения, если для Ownership используется значение autodetected). В данном разделе мы рассмотрим все составляющие, из которых получилось итоговое решение. Ну и само результирующее решение. Будут показаны максимально очищенные от всех отвлекающих внимание деталей фрагменты кода. Если кого-то интересует реальный код, то увидеть его можно здесь. Показанное ниже решение не претендует на красоту, идеальность или образец для подражания. Оно было найдено, реализовано, протестировано и задокументированно за небольшое время, под давлением сроков. Возможно, если бы времени было больше, и поиском решения занимался более молодой, толковый и сведующий в современном C++ разработчик, то оно получилось бы компактнее, проще и понятнее. Но, как получилось, так и получилось… "Don't shoot the pianist", в общем. Итак, нам нужно иметь класс с несколькими наборами методов. Содержимое этих наборов должно откуда-то взяться. Откуда? В языке D мы могли бы воспользоваться static if и определить разные части класса в зависимости от разных условий. В каком-нибудь Ruby мы могли бы подмешать методы в свой класс посредством метода include. Но мы в C++, в котором пока наши возможности сильно ограничены: мы можем либо определить метод/атрибут прямо внутри класса, либо можем унаследовать метод/атрибут из какого-то базового класса. Определить разные методы/атрибуты внутри класса в зависимости от какого-то условия мы не можем, т.к. C++ный if constexpr — это не D-шный static if. Следовательно, остается только наследование. В C++ мы можем определить несколько базовых классов, от которых мы затем отнаследуем message_holder_t. А выбор того или иного базового класса уже будем делать в зависимости от значений параметров шаблона, посредством std::conditional. Но фокус в том, что нам потребуется не просто набор базовых классов, а небольшая цепочка наследования. В ее начале будет класс, который будет определять общую функциональность, которая потребуется в любом случае. Далее будут базовые классы, которые будут определять логику поведения "умного указателя". А уже затем будет класс, который определит нужные getter-ы. В таком порядке мы и рассмотрим реализованные классы. Нашу задачу упрощает то, что в SObjectizer-е уже есть готовая шаблонная магия, которая определяет, наследуется ли сообщение от message_t, а также средства для проверки мутабельности сообщений. Поэтому в реализации мы эту готовую магию будем просто использовать и не станем погружаться в детали ее работы. Начнем с общего базового типа, который хранит соответствующий intrusive_ptr, а также предоставляет общий набор методов, которые нужны любой из реализаций message_holder_t: У этого шаблонного класса два параметра. Первый, Payload, задает тип, который должны использовать методы-getter-ы. Тогда как второй, Envelope, задает тип для intrusive_ptr. В случае, когда тип сообщения наследуется от message_t оба эти параметра будут иметь одинаковое значение. А вот если сообщение не наследуется от message_t, тогда в качестве Payload будет тип сообщения, а в качестве Envelope будет выступать user_type_message_t<Payload>. Думаю, что в основном содержимое этого класса не вызывает вопросов. Но отдельно следует обратить внимание на две вещи. Во-первых, сам указатель, т.е. атрибут m_msg, определен в protected секции для того, чтобы классы наследники имели к нему доступ. Во-вторых, для этого класса сам компилятор генерирует все необходимые конструкторы и операторы копирования/перемещения. И на уровне этого класса мы пока ничего не запрещаем. Итак, у нас есть класс, который хранит указатель на сообщение. Теперь мы можем определить его наследников, которые и будут вести себя либо как shared_ptr, либо как unique_ptr. Начнем со случая shared_ptr-поведения, т.к. здесь меньше всего кода: Ничего сложного: наследуемся от basic_message_holder_impl_t, наследуем все его конструкторы и определяем простую, неразрушающую реализацию make_reference(). Для случая unique_ptr-поведения кода побольше, хотя сложного в нем ничего нет: Опять же, наследуемся от basic_message_holder_impl_t и наследуем у него нужные нам конструкторы (это конструктор по-умолчанию и инициализирующий конструктор). Но при этом определяем конструкторы и операторы копирования/перемещения в соответствии с логикой unique_ptr: копирование запрещаем, перемещение реализуем. Также у нас здесь разрушающий метод make_reference(). Вот, собственно, все. Осталось только реализовать выбор между двумя этими базовыми классами... Для выбора между shared_ptr- и unique_ptr-поведением потребуется следующая метафункция (метафункция она потому, что "работает" с типами в компайл-тайм): Эта метафункция принимает оба параметра из списка параметров message_holder_t и в качестве результата (т.е. определения вложенного типа type) "возвращает" тип, от которого следует отнаследоваться. Т.е. либо shared_message_holder_impl_t, либо unique_message_holder_impl_t. Внутри определения impl_selector можно увидеть следы той магии, о которой говорилось выше, и в которую мы не углублялись: message_payload_type<Msg>::payload_type, message_payload_type<Msg>::envelope_type и message_mutability_traits<Msg>::mutability. А для того, чтобы использовать метафункцию impl_selector было проще, следом определим более короткое имя для нее: Итак, у нас уже есть возможность выбрать базу, которая содержит указатель и определяет поведение "умного указателя". Теперь нужно снабдить эту базу методами-getter-ами. Для чего нам потребуется один простой класс: Это шаблонный класс, который зависит от двух параметров, но их смысл уже совсем другой. В качестве параметра Base будет выступать результат показанной выше метафункции impl_selector. Т.е. в качестве параметра Base задается базовый класс, от которого нужно отнаследоваться. Важно отметить, что если наследование происходит от unique_message_holder_impl_t, у которого конструктор и оператор копирования запрещены, то компилятор не сможет сгенерировать конструктор и оператор копирования для msg_accessors_t. Что нам и требуется. В качестве параметра Return_Type будет выступать тип сообщения, указатель/ссылку на который будет возвращаться getter-ами. Фокус в том, что для иммутабельного сообщения типа Msg параметр Return_Type будет иметь значение const Msg. Тогда как для мутабельного сообщения типа Msg параметр Return_Type будет иметь значение Msg. Таким образом метод get() для иммутабельных сообщений будет возвращать const Msg*, а для мутабельных — просто Msg*. Посредством свободной функции get_ptr() решается проблема работы с сообщениями, которые не отнаследованны от message_t: Т.е. если сообщение не наследуется от message_t и хранится как user_type_message_t<Msg>, то вызывается вторая перегрузка. А если наследуется, то первая перегрузка. Итак, шаблон msg_accessors_t требует два параметра. Первый вычисляется метафункцией impl_selector. Но для того, чтобы сформировать конкретный базовый тип из msg_accessors_t, нам нужно определиться со значением второго параметра. Для этого предназначена еще одна метафункция: Обратить внимание можно разве что на вычисление параметра Return_Type. Один из тех немногих случаев, когда east const оказывается полезен ;) Ну и, для повышения читабельности последующего кода, более компактный вариант для работы с ней: Теперь можно посмотреть на то, что же из себя представляет message_holder_t, для реализации которого потребовались все эти базовые классы и метафункции (из реализации удалена часть методов для конструирования экземпляра хранящегося в message_holder-е сообщения): По сути все то, что мы разбирали выше, потребовалось для того, чтобы записать вот этот "вызов" двух метафункций: Т.к. это не первый вариант, а результат упрощения и сокращения кода, то могу сказать, что компактные формы метафункций ну очень сильно уменьшают объем кода и увеличивают его понятность (если о понятности здесь вообще уместно говорить). А вот если бы в C++ if constexpr был настолько же мощен, как static if в D, то можно было бы написать что-то вроде: Как по мне, так отличия слишком уж разительны. И они не в пользу текущего C++ :(
(разобранный выше C++ный код в виде одной сплошной "портянки" можно увидеть здесь). Кстати говоря, я не очень сильно слежу за тем, что происходит в области предложений по метапрограммированию и рефлексии для будущих версий С++. Но из того, что помню, складывается ощущение, что предлагавшиеся Саттером метаклассы не очень упростят вот эту конкретную задачу. Как я понимаю, посредством метаклассов можно будет написать генератор классов message_holder_t. Может быть такой генератор получится и несложным в написании, но вряд ли такой подход в данном конкретном случае окажется выразительнее и понятнее, чем в случае действительно продвинутого if constexpr. Как по мне, так этот пример показывает весь блеск и нищету C++. Да, можно сотворить все что угодно. В смысле, можно сделать шаблонный класс, содержимое которого будет кардинально меняться в зависимости от параметров шаблона. Но вот чтобы сделать это придется несколько поломать мозги и написать на шаблонах столько вспомогательного кода, что копаться во всем этом не будет желания даже у автора. Тем не менее, сам факт того, что на С++ можно такое сотворить, меня лично радует. Огорчает количество труда и объем кода, который для этого потребуется. Но, надеюсь, что со временем объем этого кода и его сложность будет только сокращаться. В принципе, это видно уже сейчас. Ибо для C++98/03 я даже не взялся бы такой трюк проделывать, тогда как начиная с C++11 делать подобное становится все проще и проще. Велосипедостроитель, программист-камикадзе