Неконстантные константные выражения

http://b.atch.se/posts/non-constant-constant-expressions/
  • Перевод
// <какой-то код>
 
int main ()
{
        constexpr int a = f ();
        constexpr int b = f ();
 
        static_assert (!= b, "fail");
}

Можно ли в приведенном выше фрагменте вместо комментария вставить такое определение f (), чтобы a получила значение, отличное от b?

“Разумеется, нет!” — скажете вы, немного подумав. Действительно, обе переменные объявлены со спецификатором constexpr, а значит, f () тоже должна быть constexpr-функцией. Всем известно, что constexpr-функции могут выполняться во время компиляции, и, как следствие, не должны зависеть от глобального состояния программы или изменять его (иными словами, должны быть чистыми). Чистота означает, что функция при каждом вызове с одними и теми же аргументами должна возвращать одно и то же значение. f () оба раза вызывается без аргументов, поэтому должна оба раза вернуть одно и то же значение, которое и будет присвоено переменным a и b… правильно?

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

Я ошибался.

Содержание



Дисклеймер: Техника, описанная в этой статье, позиционируется как «очередной хитрый хак с погружением в темные глубины C++». Я никому не рекомендую использовать ее в продакшене без предварительного изучения всех подводных камней (которые будут подробно описаны в следующих статьях).

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


Зачем вообще может понадобиться подобное?


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

Прим. пер: Читатель может возразить, что императивность в метапрограммы можно легко добавить, используя изменяемые макросы-константы в качестве «переменных». Так, для решения нашей задачи можно использовать #define f() __COUNTER__ или что-то подобное.

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

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

Например:

Compile-time счетчик


using C1 = ...;
 
int constexpr a = C1::next (); // = 1
int constexpr b = C1::next (); // = 2
int constexpr c = C1::next (); // = 3

Compile-time контейнер метатипов


using LX = ...;
 
LX::push<void, void, void, void> ();
LX::set<0, class Hello> ();
LX::set<2, class World> ();
LX::pop ();
 
LX::value<> x; // type_list<class Hello, void, class World>

Другие идеи




Предварительные сведения


Замечание: В этой части детально описываются технические аспекты, имеющие отношение к решению задачи. Так что наиболее опытные (или нетерпеливые) читатели могут ее пропустить. Если вы здесь только ради вкусняшек, переходите сразу к решению.
Замечание: Рекомендуется все же хотя бы бегло пролистать эту часть, если вам интересно, как именно и почему код решения работает (и является легальным с точки зрения стандарта).

Ключевое слово friend


Отношение дружбы в C++ может использоваться не только для простого предоставления другой сущности доступа к своим private и protected членам. Рассмотрим следующий (очень простой) пример:

class A;
void touch (A&);
 
class A
{
        friend void touch (A&);
        int member;
};
 
void touch (A& ref)
{
        ref.member = 123;       // OK, `void touch(A&)` является другом класса A
}
 
int main ()
{
        A a; a.member = 123;   // Некорректно, член `A::member` приватный
        A b; touch (b);      
}

Сначала мы объявляем функцию void touch (A&) в глобальной области видимости, затем объявляем ее дружественной классу A, и, наконец, определяем ее в глобальной области видимости.

С тем же успехом мы можем поместить объединенные объявление и определение void touch (A&) непосредственно внутри класса A, ничего больше не меняя — как в следующем примере:

class A
{
        friend void touch (A& ref)
        {
                ref.member = 123;
        }
 
        int member;
};
 
int main ()
{
        A b; touch (b); // OK
}

Очень важно, что эти два подхода к использованию friend не являются полностью эквивалентными (хотя в данном конкретном случае может показаться, что это так).

В последнем примере void touch (A&) будет неявно размещена в области видимости ближайшего объемлющего для класса A пространства имен, но доступ к ней будет возможен только при помощи поиска Кёнига (ADL).

class A
{
public:
        A (int);
 
        friend void touch (A) { ... }
};
 
int main ()
{
        A b = 0; touch (b); // ОК, `void touch (A)` будет найдена посредством ADL
        touch (0); // Некорректно, тип аргумента `int`, и ADL не просмотрит класс `A`
}

Приведенная ниже выдержка из стандарта повторяет вышесказанное, но я хочу, чтобы вы сконцентрировались на том, что написано между строк (потому что это в равной степени важно):

7.3.1.2/3 Определения членов пространств имен [namespace.memdef]p3
Каждое имя, впервые объявленное внутри пространства имен, является членом этого пространства имен. Если friend-объявление внутри нелокального класса впервые объявляет класс, функцию, шаблон класса или шаблон функции, они становятся членами ближайшего объемлющего пространства имен. friend-объявление само по себе не делает имя видимым для неквалифицированного (3.4.1) или квалифицированного (3.4.3) поиска имен.

Обратите внимание, что нигде не утверждается, что имя, введенное посредством friend-объявления, должно иметь какое-либо отношение к имени класса, в котором это объявление находится, да и вообще какое-либо отношение к этому классу, если уж на то пошло.

#include <iostream>
 
class A
{ 
public:
        A (int) { } 
        friend void f (A);
};
 
void g (A);
 
class B
{ 
        friend void f (A)
        {
                std::cout << "hello world!" << std::endl;
        }
 
        class C
        {
                friend void g (A)
                {
                        std::cout << "!dlrow olleh" << std::endl;
                }   
        };  
};
 
int main ()
{
        A a (0);
        f (a);
        g (1);
}

Так, f () никак не связана с классом B, за исключением того факта, что она определена как friend прямо внутри него, и такой код является абсолютно корректным.

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

Правила для константных выражений


Существует большое количество правил, связанных с constexpr, и можно было бы сделать это введение более строгим и детальным, но я буду краток:

  • Литеральным называется тип, который могут иметь constexpr-переменные и возвращаемые constexpr-функциями значения. Такими типами являются: скалярные типы (арифметические типы, указатели и перечисления), ссылочные типы и некоторые другие. Кроме того, классы, удовлетворяющие определенным ограничениям (constexpr-конструктор, тривиальный деструктор, типы всех членов-данных и базовые классы являются литеральными) также являются литеральными типами;
  • Переменная, объявленная со спецификатором constexpr, должна иметь литеральный тип и сразу быть инициализирована константным выражением;
  • Функция, объявленная со спецификатором constexpr, должна принимать в качестве параметров и возвращать только литеральные типы, а также не должна содержать конструкции, которые запрещены в константных выражениях (например, вызовы не constexpr-функций).
  • Константным называется выражение, в котором не встречаются:
    • Вызовы не constexpr-функций;
    • Вызовы функций, которые еще не определены;
    • Вызовы функций с аргументами, которые не являются константными выражениями;
    • Обращения к переменным, которые не были инициализированы константными выражениями и начали существовать до начала вычисления текущего константного выражения;
    • Конструкции, вызывающие неопределенное поведение;
    • Лямбда-выражения, возбуждения исключений и некоторые другие конструкции.

Замечание: Этот список является неполным и нестрогим, но дает представление о том, как ведут себя constexpr-сущности и константные выражения в большинстве случаев.

Вместо глубокого исследования всех аспектов работы с константными выражениями, мы сфокусируемся на правиле, которое требует, чтобы константные выражения не содержали вызовов функций, которые к моменту вызова еще не определены.

constexpr int f ();
void indirection ();
 
int main ()
{
        constexpr int n = f (); // Некорректно, `int f ()` еще не определена
        indirection ();
}
 
constexpr int f ()
{
        return 0;
}
 
void indirection ()
{
        constexpr int n = f (); // ОК
}

Здесь constexpr-функция int f () является объявленной, но не определенной в main, но имеет определение внутри indirection (поскольку к моменту начала тела indirection определение уже предоставлено). Поэтому внутри indirection вызов constexpr-функции f () будет константным выражением, а инициализация n — корректной.

Как проверить, что выражение является константным


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

Опытный C++-разработчик немедленно увидит здесь возможность удачно применить концепцию SFINAE (Substitution Failure Is Not An Error) и будет прав; но мощь, которую предоставляет SFINAE, сопровождается необходимостью написания довольно сложного кода.

Прим. пер:
Например, такого
constexpr int x = 7;
 
template <typename> 
std::false_type isConstexpr (...);
 
template <typename T, T test = (15*25 - x)> //Проверяемое выражение
std::true_type isConstexpr (*);
 
constexpr bool value = std::is_same <decltype (isConstexpr <int> (nullptr)), std::true_type>::value; //true

Гораздо проще для решения нашей задачи использовать оператор noexcept. Этот оператор возвращает true для выражений, которые не могут возбуждать исключения, и false в противном случае. В частности, все константные выражения считаются не выбрасывающими исключения. На этом мы и сыграем.

constexpr int f (); 
void indirection (); 
 
int main ()
{
        // `f()` не является константным выражением,
        // пока ее определение отсутствует
 
        constexpr bool b = noexcept (()); // false
}
 
constexpr int f ()
{
        return 0;
}
 
void indirection ()
{
        // А теперь является
 
        constexpr bool b = noexcept (()); // true
}

Замечание: в настоящее время clang содержит баг, из-за которого noexcept не возвращает true, даже если проверяемое выражение является константным. Обходной путь можно найти в приложении к этой статье.

Семантика инстанцирования шаблонов


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

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

Замечание: Учтите, что содержимое этого раздела не является полным справочником по инстанцированию шаблонов. Из упомянутых в нем правил есть исключения, кроме того, я намеренно опустил некоторые факты, которые выходят слишком далеко за рамки статьи.

Основные принципы


Словарик для самых маленьких
  • Специализация шаблона — реализация, полученная из шаблона заменой шаблонных параметров на конкретные аргументы. template <typename T> class Foo — шаблон. Foo <int> — его специализация. Программист может самостоятельно предоставить полную или частичную специализацию шаблона для определенных наборов аргументов, если необходимо, чтобы ее поведение отличалось от обобщенного. Частичная специализация недоступна для шаблонов функций.
  • Инстанцирование специализации шаблона — получение компилятором кода специализации из обобщенного кода шаблона. Для краткости часто говорят об инстанцировании шаблона с определенными аргументами, опуская слово «специализация».
  • Инстанцирование может быть явным или неявным. При явном инстанцировании программист самостоятельно сообщает компилятору о необходимости инстанцировать шаблон с определенными аргументами, например: template class Foo <int>. Кроме того, компилятор может самостоятельно неявно инстанцировать специализацию, если ему это потребуется.

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

  • Специализация шаблона (как класса, так и функции) может быть неявно инстанцирована, только если она ранее не была явно инстанцирована, и пользователь не предоставил соответствующей полной специализации.
  • Специализация шаблона функции будет неявно инстанцирована, если она упоминается в контексте, требующем ее определения, и при этом выполнен первый пункт.
  • Специализация шаблона класса будет неявно инстанцирована, если она упоминается в контексте, который требует полностью определенного (complete) типа, либо в случае, когда эта полнота влияет на семантику программы, и при этом (в обоих случаях) выполнен первый пункт.
  • Инстанцирование шаблона класса вызывает неявное инстанцирование объявлений всех его членов, но не их определений, за исключением случаев, когда член является:
    • Статическим членом-переменной, в случае которой объявление также не будет инстанцировано, или;
    • Перечислением без области видимости или анонимным объединением, в случае которых будут инстанцированы как объявление, так и определение.
  • Определения членов будут неявно инстанцированы, когда будут запрошены, но не раньше.
  • Если специализация шаблона класса содержит friend-объявления, имена этих друзей рассматриваются далее так, как будто их явные специализации были размещены в точке инстанцирования.

Точки инстанцирования


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

В случае вложенных шаблонов, когда специализация X внутреннего шаблона упоминается в контексте, зависящем от параметров внешнего шаблона Y, точка инстанцирования этой специализации будет зависеть от соответствующей точки для внешнего шаблона:

  • Если X является специализацией шаблона функции, точка инстанцирования будет совпадать с точкой инстанцирования Y.
  • Если X является специализацией шаблона класса, точка инстанцирования будет находиться непосредственно перед точкой инстанцирования Y.

Если же вложенных шаблонов нет, или контекст не зависит от параметров внешнего шаблона, точка инстанцирования будет привязана к точке D объявления/определения «наиболее глобальной» сущности, внутри которого упоминалась специализация X:

  • Если X является специализацией шаблона функции, точка инстанцирования будет находиться сразу после D.
  • Если X является специализацией шаблона класса, точка инстанцирования будет находиться непосредственно перед D.

Генерация специализации шаблона функции


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

Для простоты стандарт C++ предписывает считать, что у любой инстанцированной специализации шаблона функции есть дополнительная точка инстанцирования в конце единицы трансляции:

namespace N
{
        struct X { /* Специально оставлена пустой */ };
 
        void func (X, int);
}
 
template <typename T>
void call_func (T val)
{
        func (val, 3.14f);
}
 
int main ()
{
        call_func (N::X {});
}
 
//Первая точка инстанцирования
 
namespace N
{
        float func (X, float);
}
 
//Вторая точка инстанцирования

В этом примере у нас есть две точки инстанцирования функции void call_func <N::X> (N::X). Первая находится сразу после определения main (потому что call_func вызывается внутри нее), а вторая — в конце файла.

Приведенный пример является некорректным из-за того, что поведение call_func <N::X> изменяется в зависимости от того, в какой из них компилятор сгенерирует код специализации:

  • В первой точке call_func вызовет func (X, int), потому что другие перегрузки в этот момент еще отсутствуют.
  • Во второй точке call_func вызовет уже объявленную к этому моменту func (X, float), как наиболее подходящую из всех доступных перегрузок.

Генерация специализации шаблона класса


Для специализации шаблона класса все точки инстанцирования, кроме первой, игнорируются. Это фактически означает, что компилятор должен сгенерировать код специализации во время первого ее упоминания в требующем инстанцирования контексте.

namespace N
{
        struct X { /* Специально оставлена пустой */ };
 
        void func (X, int);
}
 
template<typename  T> struct A { using type = decltype (func (T{}, 3.14f)); };
template<typename T> struct B { using type = decltype (func (T{}, 3.14f)); };
 
//Точка инстанцирования A
 
int main ()
{
        A<N::X> a;
}
 
namespace N
{
        float func (X, float);
}
 
//Точка инстанцирования B
 
void g ()
{
        A<N::X>::type a; // Некорректно, type будет `void`
        B<N::X>::type b; // OK, type будет `float`
}

Здесь точка инстанцирования A <N::X> будет находиться непосредственно перед main, в то время как точка инстанцирования B <N::X> — только перед g.

Собираем все воедино


Правила, связанные с friend-объявлениями внутри шаблонов классов, утверждают, что в следующем примере определения func (short) и func (float) генерируются и размещаются в точках инстанцирования соответственно специализаций A <short> и A <float>.

constexpr int func (short);
constexpr int func (float);
 
template <typename T>
struct A
{ 
        friend constexpr int func (T) { return 0; }
};
 
template <typename T>
<T> indirection ()
{
        return {};
}
 
// (1)
 
int main ()
{
        indirection <short> (); // (2)
        indirection <float> (); // (3)
}

Когда вычисляемое выражение содержит вызов специализации шаблона функции, возвращаемый тип этой специализации должен быть полностью определен. Поэтому вызовы в строках (2) и (3) влекут за собой неявное инстанцирование специализаций A вместе со специализациями indirection перед их точками инстанцирования (которые, в свою очередь, находятся перед main).

Важно, что до достижения строк (2) и (3) функции func (short) и func (float) объявлены, но не определены. Когда достижение этих строк вызовет инстанцирование специализаций A, определения этих функций появятся, но будут расположены не рядом с этими строками, а в точке (1).


Решение


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

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

  • Ключевое слово friend, и;
  • Правила для константных выражений, и;
  • Семантика инстанцирования шаблонов.

Реализация


constexpr int flag (int);
 
template <typename Tag>
struct writer
{
        friend constexpr int flag (Tag)
        {
                return 0;
        }
};
 
template <bool B, typename Tag = int>
struct dependent_writer : writer <Tag> { };
 
template <
        bool B = noexcept (flag (0)),
        int = sizeof (dependent_writer <B>)
>
constexpr int f ()
{
        return B;
}
 
int main ()
{
        constexpr int a = f ();
        constexpr int b = f ();
 
        static_assert (!= b, "fail");
}

Замечание: clang демонстрирует некорректное поведение с этим кодом, обходной путь доступен в приложении.
Прим. пер: Визуальные подсказки в Visual Studio 2015 также «не замечают» изменений в f (). Тем не менее, после компиляции значения a и b различаются.

Но как это работает?


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

В этом разделе пошагово разбирается исходный код и дается краткое описание и обоснование для каждого его фрагмента.

“Переменная”


В каждой точке программы constexpr-функция может находиться в одном из двух состояний: либо она уже определена и может вызываться из константных выражений, либо нет. Возможна только одна из этих двух ситуаций (если не допускать неопределенного поведения).

Обычно constexpr-функции рассматриваются и используются именно как функции, однако благодаря вышесказанному мы можем думать о них как о “переменных”, которые имеют тип, похожий на bool. Каждая такая “переменная” находится в одном из двух состояний: “определена” или “не определена”.

constexpr int flag (int);

В нашей программе функция flag является просто подобным триггером. Мы нигде не будем вызывать ее как функцию, интересуясь только ее текущим состоянием как “переменной”.

Модификатор


writer это шаблон класса, который при инстанцировании создает определение для функции в своем объемлющем пространстве имен (в нашем случае — глобальном). Параметр шаблона Tag определяет конкретную сигнатуру функции, определение которой будет создано:

template <typename Tag>
struct writer
{
        friend constexpr int flag (Tag)
        {
                return 0;
        }
};

Если мы, как и собирались, будем рассматривать constexpr-функции как “переменные”, то инстанцирование writer с аргументом шаблона T вызовет безусловный перевод “переменной” с сигнатурой int func (T) в положение “определена”.

Прокси


template <bool B, typename Tag = int>
struct dependent_writer : writer <Tag> { };

Я не удивлен, если вы решили, что dependent_writer выглядит как бессмысленная прослойка, добавляющая косвенности. Почему бы напрямую не инстанцировать writer <Tag> там, где мы хотим изменить значение “переменной”, вместо обращения к нему через dependent_writer?

Дело в том, что прямое обращение к writer <int> не гарантирует, что первый аргумент шаблона функции f будет вычислен раньше второго (и функция при первом вызове успеет “запомнить”, что нужно вернуть false, а уже потом изменит значение “переменной”).

Чтобы задать нужный нам порядок вычисления аргументов шаблона, можно добавить дополнительную зависимость при помощи dependent_writer. Первый шаблонный аргумент dependent_writer должен быть вычислен до его инстанцирования, и, следовательно, до инстанцирования самого writer. Поэтому, передавая B в dependent_writer в качестве аргумента, мы можем быть уверены, что к моменту инстанцирования writer возвращаемое функцией f значение уже будет вычислено.

Замечание: При написании реализации я рассматривал множество альтернатив, пытаясь найти наиболее простую для понимания. Я искренне надеюсь, что этот фрагмент получился не слишком запутанным.

Магия


template <
        bool B = noexcept (flag (0)),            // (1)
        int = sizeof (dependent_writer <B>) // (2)
>
constexpr int f ()
{
        return B;
}

Этот фрагмент может показаться немного странным, но на самом деле он очень прост:

  • (1) устанавливает B равным true, если flag (0) является константным выражением, и равным false, если нет;
  • (2) неявно инстанцирует dependent_writer <B> (оператор sizeof требует полностью определенного типа). При этом инстанцируется и writer <B>, что, в свою очередь, вызывает генерацию определения int flag (int) и изменение состояния нашей “переменной”.

Поведение можно выразить следующим псевдокодом:

ЕСЛИ [ `int flag (int)` еще не определена ]:
        УСТАНОВИТЬ `B` = `false`
        ИНСТАНЦИРОВАТЬ `dependent_writer <false>`
        ВЕРНУТЬ `B`
ИНАЧЕ:
        УСТАНОВИТЬ `B` = `true`
        ИНСТАНЦИРОВАТЬ `dependent_writer <true>`
        ВЕРНУТЬ `B`

Таким образом, при первом вызове f шаблонный аргумент B будет равен false, но побочным эффектом вызова f станет изменение состояния “переменной” flag (генерация и размещение ее определения перед телом main). При дальнейших вызовах f “переменная” flag будет уже в состоянии “определена”, поэтому B будет равно true.


Заключение


То, что люди продолжают открывать сумасшедшие способы сделать при помощи C++ новые вещи (которые раньше считались невозможными), является одновременно удивительным и ужасным. — Морис Бос

В этой статье объясняется базовая идея, позволяющая добавить состояние константным выражениям. Иными словами, общепринятая теория (к которой часто обращался и я сам) о том, что константные выражения являются “константными”, теперь разрушена.

Прим. пер: На мой взгляд, «разрушена» — чересчур сильное слово. Все таки, несмотря на идентичность имен, в решении вызываются две различные специализации шаблонной функции f (), каждая из которых является полноценно «константной». Разумеется, это не умаляет общей полезности идеи.

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

Я написал библиотеку для императивного шаблонного метапрограммирования, названную smeta, которая будет опубликована, объяснена и обсуждена в предстоящих статьях. Среди тем, которые будут охвачены:

  • Как реализовать compile-time счетчик
  • Как реализовать compile-time контейнер метатипов
  • Как проверять разрешения перегрузок и управлять ими
  • Как добавить рефлексию в C++

Прим. пер: Автор сообщает, что решил отменить релиз smeta, т.к. эта и последующие статьи содержат (либо будут содержать) практически все ее фичи, и, следовательно, самостоятельная реализация читателем ее функциональности для себя будет практически тривиальна. Я, например, уже (вопреки предостережению Филиппа, ага) внедрил некоторые идеи и собираюсь собрать их в нечто библиотекоподобное в перспективе.


Приложение


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

namespace detail
{
        struct A
        {
                constexpr A () { }
                friend constexpr int adl_flag (A);
        };
 
        template <typename Tag>
        struct writer
        {
                friend constexpr int adl_flag (Tag)
                {
                        return 0;
                }
        };
}
 
template <typename Tag, int = adl_flag (Tag {})>
constexpr bool is_flag_usable (int)
{
        return true;
}
 
template <typename Tag>
constexpr bool is_flag_usable (...)
{
        return false;
}
 
template <bool B, class Tag = detail::A>
struct dependent_writer : detail::writer <Tag> { };
 
template <
        class Tag = detail::A,
        bool B = is_flag_usable <Tag> (0),
        int = sizeof (dependent_writer <B>)
>
constexpr int f ()
{
        return B;
}
 
int main ()
{
        constexpr int a = f ();
        constexpr int b = f ();
 
        static_assert (!= b, "fail");
}

Замечание: В настоящее время я пишу соответствующие баг-репорты, которые будут показывать, почему именно такое обходное решение работоспособно в clang. Ссылки на них будут добавлены, как только репорты будут поданы (пока все глухо — прим. пер.).

Раздел благодарностей из оригинальной статьи

Благодарности


Есть много людей, без которых эта статья не была бы написана, но я особенно благодарен:

  • Морису Босу, за:
    • Помощь с формулировкой идей.
    • Предоставление мне средств на покупку контактных линз, что избавило меня от необходимости (буквально) писать вслепую.
    • Терпимость к моим приставаниям по поводу его мнения о статье.
  • Михаэлю Килпелайнену, за:
    • Корректуры, а также интересные мысли о том, как сделать эту статью более понятной.
  • Columbo, за:
    • Безуспешные попытки доказать, что описанная техника порождает некорректные программы (путем бросания параграфов стандарта C++ мне в лицо). Если что, я бы сделал то же самое для него.


От переводчика


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

Эта статья является первой в серии статей об императивном метапрограммировании, которые развивают идею неконстантных constexpr’ов (непосредственная применимость которых на практике сильно ограничена) до реализации конструкций, которые могут сделать реальное метапрограммирование существенно удобнее.

Я собираюсь в ближайшее время перевести две оставшиеся статьи (о compile-time счетчике и о контейнере метатипов). Кроме того, Филипп сказал, что в обозримом будущем продолжит серию и разовьет идеи дальше (тогда, разумеется, будут и новые переводы).

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

Поделиться публикацией
Комментарии 28
    +17
    Боже мой, похоже что Филипп Розеен это еще один Александреску :)
    Самое страшное, что оно сейчас расползется, все начнут это использовать, потом в Бусте что нибудь появится, и потом это станет нормой.
      +1
      Так никто ж не заставляет вас это трогать. Если бы такая фишка как «счетчки времени компиляции» была встроенна в язык, это было бы еще большей магией.

      Так что пусть живет.
        0
        Это не было бы магией, хотя не было бы и полным решением проблемы («счетчики времени компиляции» — лишь крошечная часть того, что можно сделать, получив доступ к императивному метапрограммингу).
        Я вот в предыдущем посте по С++ высказался насчет того, как я вижу идеальный вариант метапрограммирования — так заминусовали. А эта дикая смесь constexpr, шаблонов и friend объявлений уже есть, и ей будут пользоваться, получая в очередной раз километровые сообщения об ошибках компиляции. И трогать придется, потому что реальные задачи требуют хоть каких-то решений, и за неимением других придется пользоваться этим.
          +4
          Там заминусовали за упоминание javascript в в контексте святая святых плюсов — компилятора. Тема с прямым доступом к AST была, вроде, нормально воспринята.
            +2
            Именно так. «Дикую смесь constexpr, шаблонов и friend объявлений» именно потому и терпят, что она «уже есть» — как известно метопрограммирование в C++ было не сознательно добавлено, оно было открыто! Если уж сознательно добавлять — то нужно делать «по уму», а не путём вкручивания в язык, где и так полно костылей, ещё одного, причём весьма немаленького масштаба.

            Да, это правда, что на стандартном C++ (без дополнительных библиотек) тяжело работать с AST, деревьями и прочими подобными вещами. Но это нифига не повод вкручивать в компилятор совершенно другой язык с совершенно другими «правилами игры» — это повод расширить стандартную библиотеку, не более того.
            +1
            Концепция С++ мне видится в том, что в ядре языка находится минимум функциональности, и та ее часть, которая в других случаях переносистя в ядро (ну, поддержка модификации AST — Тоже фича ядра языка), находится в стандартной библиотеке. Если же вам нужен доступ к AST, то переходите на Erlang :)

            А про «трогать» я имел в виду реализцию. Кто-то в бусте реализовал это — ну и пусть себе там будет. Вам-то какое дело, как оно там устроено? Длинные ошибки компиляции сейчас, тем более, в принципе исправлюятся static_assert-ами.

            +6
            Зато с языком, в котором нет магии, вы не сможете почувствовать себя волшебником :)
          +1
          А потом Филипп устает от этой магии и пойдёт пилить язык E основной фишкой которого станет императивное программирование времени компиляции :-)
          0
          Я дико извиняюсь, но что означает конструкция int = sizeof (dependent_writer <B>) ?
            +2
            Тут просто имя пропущено. Оно ведь не нужно, параметр все равно нигде не используется. По сути это просто еще один шаблонный параметр со значением по умолчанию.

            Вот тут, например, видно, что имя только опционально указывается в этом синтаксисе.
              0
              Жуть. Спасибо за объяснение.
            +2
            Я наконец-то понял, что мне эта техника напоминает!

            image
              +1
              Эту технику надо в тот список вредных советов, про который тут статья недавно была.
                0
                За наличие таких техник бить по лицу ссаными тряпками авторов кривых стандартов надо. И запрещать к чертям не прикрываясь идиотским «ни можем, совместимость поломаем, агу-агу».
                +5
                Шикарная статья! И переводчик не подкачал. При всей сложности вопроса всё описано настолько понятно, что просто диву даёшься. Жду перевода остальных статей!
                  0
                  Я не силен в метамагии, но насчет прослойки dependent_writer — может можно убрать вот так:
                  constexpr int flag(int);
                  
                  template <bool b>
                  struct writer
                  {
                      friend constexpr int flag()
                      {
                          return 0;
                      }
                  };
                  
                  template <
                      bool B = noexcept (flag(0)),
                      typename T = writer <B>
                  >
                  constexpr int f()
                  {
                      return B;
                  }
                  
                  int main()
                  {
                      constexpr int a = f();
                      constexpr int b = f();
                  
                      static_assert (a != b, "fail");
                  }
                  
                    0
                    Однако же не работает! (gcc-4.8.2)
                    t4.cc:29:5: error: static assertion failed: fail
                    static_assert (a != b, «fail»);
                    Видимо выражение typename T=writer<B> не вычисляется т.к. Т не используется.

                      0
                      Да, здесь получается контекст, который не требует, чтобы writer <B> был complete type. Поэтому и использовался sizeof, для принудительного инстанцирования.

                      Но вообще это довольно интересный вопрос — почему бы не добавить зависимость от B в сам writer. Если сделать, как выше написали, (т.е. совсем убрать Tag из writer, а вместо него добавить B), то получится, что каждая из специализаций writer (для true и для false) попытается создать определение для flag, и нарушится one definition rule. То же самое произойдет, если оставить в writer оба параметра (Tag и B) — для каждого их набора будет попытка сгенерировать отдельное определение flag, и уже во время второй генерации все покрашится.

                      А dependent_writer все свои специализации (и для true == B, и для false == B) наследует от единственной специализации writer <int>, поэтому определение flag будет сгенерировано только однажды, и ODR не нарушится.
                        0
                        Надо обдумать не спеша, и вообще мне кажется если завернуть это покрасивее то уже вполне можно пользоваться.
                        Вот только еще осознать где и как это лучше применить.
                        0
                        Не работает — это в смысле работает?) Ведь целью и было получить заданный assert. Я проверял на VC++15
                          0
                          Нет. Assert не вывалится, когда a не равно б, что и явыляется целью
                            0
                            По-моему Вы путаете — цель статьи показать, что constexpr-функции могут возвращать разные значения. Для этого и добавлен assert(a!=b) — чтобы показать, что можно добиться разных значений. И этот assert должен сработать, чтобы показать, что мы таки получили разные значения.
                              0
                              нет. он сработает, если значение одинаковые.

                              assert(5 != 5); //сработает
                              assert(4 != 5); //не сработает
                              
                                0
                                Наверное, насчет static_assert немного двусмысленно получилось в статье. Под «срабатыванием» подразумевалось именно прохождение ассерта, т.е. нормальная компиляция без ошибки. А ошибки не будет, если условие ассерта true, чего нам и надо. Иначе говоря, если код работает, и значения различаются, то все компилируется нормально. Я перефразировал в самой статье и обновил.
                                  0
                                  Да, согласен. Извините, запутался.
                        +2
                        Спасибо за перевод, давно на Хабре таких жемчужин не было!

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое