Как можно перечислять, упорядочивать и т. Д. Классы во время компиляции?
Я борюсь с некоторыми правилами того, что может быть использовано в вычислениях времени компиляции. Здесь я написал код, который связывает уникальный идентификатор с каждым классом, который запрашивает его (и выделенное имя для целей тестирования). Однако этот уникальный идентификатор нельзя использовать в качестве аргумента шаблона или части условия 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. Тем не менее, я не вижу, как это улучшение по сравнению с ручным назначением идентификаторов. Если бы вам пришлось отсортировать один список классов по их идентификаторам, а затем обернуть каждый из классов и запросить идентификаторы для упаковщиков, идентификаторы будут зависеть от сортировки; Затем, чтобы определить "последний" элемент, вам придется отсортировать элементы вручную! Что мне не хватает?
2 ответа
Это очень интересный вопрос, поскольку он связан не только с реализацией счетчика во время компиляции в C++, но и с ассоциативным (статическим) значением счетчика с типами во время компиляции.
Поэтому я немного исследовал и наткнулся на очень интересную запись в блоге. Как реализовать счетчик постоянных выражений в C++. Автор Filip Roséen - refp
Его реализация счетчика действительно расширяет границы 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.
- Будьте осторожны с некоторыми операторами сравнения между значениями, сгенерированными constexpr; несмотря на порядок ваших вызовов, иногда нет никаких гарантий относительно времени, в течение которого компилятор будет их создавать. (мы могли бы сделать что-нибудь об этом с
std::atomic
?)- Это означает
a < b
не гарантируется, что оно будет истинным, если оно будет оценено во время компиляции, даже если это будет во время выполнения.
- Это означает
- Порядок подстановки аргументов шаблона; может привести к противоречивому поведению в компиляторах C++11; исправлено в C++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>)
для каждого целочисленного значения; один для каждого неиспользованного значения счетчика на самом деле.
Иногда нужно признать, что 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
(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. Но я думаю, что это тоже решаемая проблема...
Пост скриптум:
Возможно, все еще возможно осуществить это, используя только C++, используя специфичные для компилятора расширения. Например, используя макрос __COUNTER__ для gcc.