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

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

#include <cassert>
#include <cxxabi.h>
#include <iostream>
#include <typeinfo>

namespace UID {
    static int nextID(void) {
        static int stored = 0;
        return stored++;
    }
    template<class C>
    static int getID(void) {
        static int once = nextID();
        return once;
    }
    template<class C>
    static const char *getName(void) {
        static int status = -4;
        static const char *output =
            abi::__cxa_demangle(typeid(C).name(), 0, 0, &status);
        return output;
    }
}

namespace Print {
    template<class C>
    std::ostream& all(std::ostream& out) {
        return out << "[" << UID::getID<C>() << "] = "
            << UID::getName<C>() << std::endl;
    }
    template<class C0, class C1, class... C_N>
        std::ostream& all(std::ostream& out) {
        return all<C1, C_N>(all<C0>(out));
    }
}

void test(void) {
    Print::all<int, char, const char*>(std::cout) << std::endl;
    // [0] = int
    // [1] = char
    // [2] = char const*
    Print::all<char, int, const char*>(std::cout);
    // [1] = char
    // [0] = int
    // [2] = char const*
}

Если это неясно, я хотел бы изменить другое поведение во время компиляции на основе идентификатора. Я видел несколько подходов, в которых используется связанный список типов, так что идентификатор представляет собой сумму ранее назначенного идентификатора constexpr и смещения constexpr. Однако я не вижу, чем это лучше, чем назначение идентификаторов вручную. Если бы вам нужно было отсортировать один список классов по их идентификаторам, а затем обернуть каждый из классов и запросить идентификаторы для оберток, идентификаторы будут зависеть от сортировки; тогда, чтобы определить «последний» элемент, вам придется либо отсортировать элементы вручную! Что мне не хватает?


person John P    schedule 29.12.2015    source источник
comment
Какая связь с tmp? Я не понимаю.   -  person YSC    schedule 30.12.2015
comment
@YSC Вероятно, они имели в виду метапрограммирование шаблонов.   -  person Captain Obvlious    schedule 30.12.2015
comment
Я призываю вас обратить внимание на теги, которые вы используете.   -  person Lightness Races in Orbit    schedule 30.12.2015
comment
Да это оно. Спасибо за редактирование.   -  person John P    schedule 30.12.2015
comment
@JohnP: Если вы можете переварить предупреждения, то как насчет чего-то вроде этого?   -  person AndyG    schedule 30.12.2015
comment
Потрясающий! Я хотел бы принять это как ответ, не могли бы вы скопировать его сюда?   -  person John P    schedule 31.12.2015


Ответы (2)


Это очень интересный вопрос, потому что он связан не только с реализацией счетчика во время компиляции в C++, но и с ассоциированием (статических) значений счетчика с типами во время компиляции.

Итак, я провел небольшое исследование и наткнулся на очень интересный пост в блоге Как реализовать счетчик константных выражений. на C++ Филип Розен

Его реализация счетчика действительно расширяет пределы работы ADL и SFINAE:

template<int N>
struct flag {
  friend constexpr int adl_flag (flag<N>);
};
template<int N>
struct writer {
  friend constexpr int adl_flag (flag<N>) {
    return N;
  }

  static constexpr int value = N;
};
template<int N, int = adl_flag (flag<N> {})>
int constexpr reader (int, flag<N>) {
  return N;
}

template<int N>
int constexpr reader (float, flag<N>, int R = reader (0, flag<N-1> {})) {
  return R;
}

int constexpr reader (float, flag<0>) {
  return 0;
}
template<int N = 1>
int constexpr next (int R = writer<reader (0, flag<32> {}) + N>::value) {
  return R;
}
int main () {
  constexpr int a = next ();
  constexpr int b = next ();
  constexpr int c = next ();

  static_assert (a == 1 && b == a+1 && c == b+1, "try again");
}

По сути, он основан на том, что ADL не может найти подходящее определение функции friend, что приводит к SFINAE, и повторяется с шаблонами до тех пор, пока либо точное совпадение, либо ADL не будет успешным. Сообщение в блоге довольно хорошо объясняет, что происходит.

Ограничения

(взято из статьи)

  • Вы не можете использовать один и тот же счетчик в единицах перевода, иначе вы можете нарушить ODR.
  • Be careful with some comparison operators between constexpr generated values; despite the order of your calls there are sometimes no guarantees on the relative time the compiler will instantiate them. (could we do anything about this with std::atomic?)
    • This means a < b is not guaranteed to be true if evaluated at compile-time, even though it will be by run time.
  • порядок подстановки аргументов шаблона; может привести к непоследовательному поведению компиляторов C++11; исправлено в С++ 14
  • Поддержка MSVC: даже компилятор, поставляемый с Visual Studio 2015, по-прежнему не имеет полной поддержки выражения SFINAE. Обходные пути доступны в сообщении в блоге.

Превращение счетчика в UUID, связанный с типом

Оказывается, это было действительно довольно просто изменить:

template<int N = 1, int C = reader (0, flag<32> ())>
int constexpr next (int R = writer<C + N>::value) {
  return R;
}

в

template<typename T, int N = 1>
struct Generator{
 static constexpr int next = writer<reader (0, flag<32> {}) + N>::value; // 32 implies maximum UUID of 32
};

Учитывая, что const static int — это один из немногих типов, которые вы можете объявить и определить в одном и том же месте [9.4.2.3]:

Статический элемент данных литерального типа может быть объявлен в определении класса с помощью спецификатора constexpr; если это так, его объявление должно указывать инициализатор-или-равно-скобке, в котором каждое предложение-инициализатора, являющееся выражением-присваивания, является константным выражением. [Примечание: в обоих этих случаях член может появляться в постоянных выражениях. — примечание в конце]

Итак, теперь мы можем написать такой код:

constexpr int a = Generator<int>::next;
constexpr int b = Generator<int>::next;
constexpr int c = Generator<char>::next;

static_assert(a == 1, "try again");
static_assert(b == 1, "try again");
static_assert(c == 2, "try again");

Обратите внимание, что int остается 1, а char увеличивает счетчик до 2.

Текущая демонстрация

Этот код страдает от всех тех же недостатков, что и раньше (и, возможно, больше, о чем я не думал).

Примечание

С этим кодом будет большое количество предупреждений компилятора из-за большого количества объявлений friend constexpr int adl_flag(flag<N>) для каждого целочисленного значения; фактически по одному на каждое неиспользуемое значение счетчика.

person AndyG    schedule 31.12.2015
comment
Я немного протестировал. Он не работает при компиляции с GCC 8 и выше. У вас есть идеи, почему? И как мы можем это исправить? - person NutCracker; 01.10.2020
comment
@nutcracker похоже, что мы не можем гарантировать, когда здесь создается экземпляр типа относительно другого. Я не уверен, что это действительно гарантированно сработает, хотя связанная статья приводит веские аргументы в пользу этого. - person AndyG; 02.10.2020

Иногда приходится признать, что C++ сам по себе не решит всех мировых проблем.

Иногда возникает необходимость интегрировать в свою систему сборки дополнительные инструменты и скрипты. Думаю, это один из таких случаев.

Но сначала давайте воспользуемся только C++, чтобы решить как можно большую часть этой проблемы. И мы будем использовать Любопытно рекурсивный шаблон шаблона:

template<typename C> class UID {

public:

    static const int id;
};

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

class Widget : public UID<Widget> {

// ...

};

Таким образом, Widget::id становится уникальным идентификатором класса.

Теперь все, что нам нужно сделать, это выяснить, как объявить значения id всех классов. И в этот момент мы достигаем пределов того, что C++ может делать сам по себе, и мы должны вызвать некоторые подкрепления.

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

Button
Field
Widget

(Кнопка, поле и виджет — это классы, отличные от наследуемых от класса UID).

Теперь это становится простым двухэтапным процессом:

1) Простая оболочка или Perl-скрипт, который читает файл classlist и выдает сгенерированный роботом код формы (учитывая приведенный выше ввод):

const int UID<Button>::id=0;
const int UID<Field>::id=1;
const int UID<Widget>::id=2;

... и так далее.

2) Соответствующие настройки вашего скрипта сборки или Makefile, чтобы скомпилировать этот сгенерированный роботом код (со всеми необходимыми #include и т. д., чтобы это произошло) и связать его с вашим приложением. Таким образом, класс, который хочет, чтобы ему был назначен идентификатор, должен явно наследоваться от класса UID, а его имя должно быть добавлено в файл. Затем сценарий сборки/Makefile автоматически запускает сценарий, который генерирует новый список uid и компилирует его во время следующего цикла сборки.

(Надеюсь, вы используете настоящую среду разработки C++, которая предоставляет вам гибкие инструменты разработки, вместо того, чтобы быть вынужденным страдать от какой-то негибкой ограниченной среды разработки типа Visual-IDE с ограниченной функциональностью).

Это всего лишь отправная точка. Приложив немного больше усилий, можно будет взять этот базовый подход и улучшить его, чтобы автоматически генерировать constexpr uid, что было бы еще лучше. Для этого потребуется взломать несколько крепких орешков, например, попытаться избежать повторной компиляции всего приложения при изменении списка классов, использующих UID. Но, думаю, это тоже решаемая проблема...

Пост скриптум:

Возможно, это все еще возможно осуществить, используя только C++, используя расширения, специфичные для компилятора. Например, с помощью макроса gcc __COUNTER__.

person Sam Varshavchik    schedule 30.12.2015
comment
Если бы классы включали X, Y‹X›, Y‹Y‹X›› и т. д. или несколько декартовых произведений «единичных» классов, вы бы просто вручную вводили их в файл, пока не подумали, что вам «достаточно»? Счетчик выглядит так, как будто он будет работать (GCC и VS поддерживают его напрямую, плюс у Boost есть «BOOST_PP_COUNTER»), но частью точки TMP является переносимость. Я в основном сбит с толку, потому что TMP также должен быть полным по Тьюрингу, но я рано и сильно ударился о кирпичную стену. - person John P; 30.12.2015
comment
Набор классов, которые необходимо перечислить, должен быть конечным набором. В C++ невозможно придумать код, использующий бесконечное количество классов. Таким образом, какие бы классы ни использовались, они должны быть перечислены. Возможно даже составить список классов непосредственно из исходного кода. т.е. class Widget : UID_DECL‹Widget› { ... с UID #define UID_DECL. Затем ваш скрипт сборки/Makefile собирает все заголовки для UID_DECL и на их основе генерирует список классов, а затем и сами идентификаторы. - person Sam Varshavchik; 30.12.2015
comment
Я не имел в виду бесконечность, просто нецелесообразно перечислять. (Почему вы думаете, что мне нужны бесконечные классы?) Почему было бы непрактично выводить их из внешнего списка? Что ж, классы, которые я хочу перечислить, могут зависеть от логики времени компиляции, включая перечисление или другие черты предшествующих классов. Внешнее перечисление классов позволяет полностью избежать логики времени компиляции. Это заставляет меня думать, что вы интерпретировали мой вопрос как перед выполнением, а не во время компиляции. Извините за путаницу... - person John P; 01.01.2016