现代C ++为我们带来了很多以前在该语言中非常缺乏的功能。 为了以某种方式获得类似的效果,长时间以来发明了令人惊叹的拐杖,主要由非常大的图案和宏的脚垫组成(通常也是自生的)。 但是现在,时不时地出现了对仍然没有用语言表达的机会的需求。 然后,我们再次从模板和宏重新发明复杂的设计,生成它们并实现所需的行为。 这就是这样一个故事。
在过去的半年中,我两次需要可以在模板参数中使用的值。 同时,我希望这些值具有易于理解的名称,而不必事先声明这些名称。 我解决的特定任务是一个单独的问题,也许以后我将在“异常编程”中心的某个位置写有关它们的单独文章。 现在,我将讨论解决这个问题的方法。
因此,涉及模板参数时,我们可以使用type或static const值。 对于大多数任务而言,这绰绰有余。 我们想在参数中使用人类可读的标识符-我们声明结构,枚举或常量并使用它们。 当我们无法提前确定此标识符并想就位时,问题就开始了。
可以直接在template参数中声明结构或类。 如果模板对此参数不执行任何要求完整说明结构的操作,这甚至将起作用。 此外,我们无法控制在其中声明这种结构的名称空间。 如果这些行位于相邻的类或名称空间中,则外观完全相同的模板替换将变成完全不同的代码。
您需要使用文字,在C ++中的所有文字中,只有字符文字和字符串文字可以被称为可读。 但是字符字面量限制为四个字符(使用char32_t时),而字符串字面量是字符数组,并且其值不能传递给模板参数。
事实证明某种恶性循环。 您必须事先声明一些内容,或者使用不方便的标识符。 让我们尝试获取不适合它的语言。 如果要实现一个宏,该宏将使某些内容适合用于字符串文字的模板参数,该怎么办?
让我们构造字符串的结构
首先,让我们为字符串做基础。 在C ++ 11中,出现了可变参数模板参数。
我们声明一个在参数中包含字符串字符的结构:
template <char... Chars> struct String{};
的github可以用 我们甚至可以立即使用以下行:
template <class T> struct Foo {}; Foo<String<'B', 'a', 'r'>> foo;
现在将此行拖到运行时
太好了 能够在运行时获取此字符串的值也不错。 让我们有一个额外的模板结构,它将从这样的字符串中提取参数,并从中获取一个常量:
template <class T> struct Get; template <char... Chars> struct Get<String<Chars...>> { static constexpr char value[] = { Chars... }; };
这也有效。 由于我们的行末尾不包含'\ 0',因此您需要仔细处理此常量(我认为最好在构造函数参数中使用其常量和sizeof来立即创建string_view)。 可以在数组末尾添加'\ 0',但这对于我的任务不是必需的。
检查我们可以操纵这样的字符串
好的,您还能用这些线做什么? 例如串联:
template <class A, class B> struct Concatenate; template <char... Chars, char... ExtraChars...> struct Concatenate<String<Chars...>, String<ExtraChars...>> { using type = String<Chars..., ExtraChars...>; };
的github原则上,您可以执行或多或少的任何操作(我没有尝试过,因为我不需要它,但是我只能想象如何搜索子字符串甚至替换子字符串)。
现在,我们有一个主要问题,即如何在编译时从字符串文字中提取字符并将其放入模板参数中。
画猫头鹰,写一个宏。
让我们从一种将字符一次放入模板参数的方式开始:
template <class T, char c> struct PushBackCharacter; template <char... Chars, char c> struct PushBackCharacter<String<Chars...>, c> { using type = String<Chars..., c>; }; template <char... Chars> struct PushBackCharacter<String<Chars...>, '\0'> { using type = String<Chars...>; };
的github我对字符“ \ 0”使用单独的特殊化,以免将其添加到所使用的字符串中。 此外,这在某种程度上简化了宏的其他部分。
好消息是,字符串文字可以作为constexpr函数的参数。 我们将编写一个函数,该函数将按字符串或“ \ 0”中的索引返回一个字符,如果该字符串比索引短(在这里,字符“ \ 0”的PushBackCharacter专长会派上用场)。
template <size_t N> constexpr char CharAt(const char (&s)[N], size_t i) { return i < N ? s[i] : '\0'; }
基本上,我们已经可以编写如下代码:
PushBackCharacter< PushBackCharacter< PushBackCharacter< PushBackCharacter< String<>, CharAt("foo", 0) >::type, CharAt("foo", 1) >::type, CharAt("foo", 2) >::type, CharAt("foo", 3) >::type
我们放了一块鞋,但是在我们的宏中放了更多真正的东西(我们可以编写脚本来生成代码),仅此而已!
有细微差别。 如果该行中的字符数大于宏中的嵌套级别,该行将被截断,我们甚至不会注意到它。 一团糟。
让我们再做一个结构,该结构不会以任何方式转换到达它的字符串,但是会进行static_assert以便其长度不超过常量:
#define _NUMBER_TO_STR(n) #n #define NUMBER_TO_STR(n) _NUMBER_TO_STR(n) template <class String, size_t size> struct LiteralSizeLimiter { using type = String; static_assert(size <= MAX_META_STRING_LITERAL_SIZE, "at most " NUMBER_TO_STR(MAX_META_STRING_LITERAL_SIZE) " characters allowed for constexpr string literal"); }; #undef NUMBER_TO_STR #undef _NUMBER_TO_STR
好了,宏看起来像这样:
#define MAX_META_STRING_LITERAL_SIZE 256 #define STR(literal) \ ::LiteralSizeLimiter< \ ::PushBackCharacter< \ ... \ ::PushBackCharacter< \ ::String<> \ , ::CharAt(literal, 0)>::type \ ... \ , ::CharAt(literal, 255)>::type \ , sizeof(literal) - 1>::type
的github原来
template <class S> std::string_view GetContent() { return std::string_view(Get<S>::value, sizeof(Get<S>::value)); } std::cout << GetContent<STR("Hello Habr!")>() << std::endl;
我的实现可以
在github上
找到 。
对于我来说,了解这种机制的可能应用与我提出的应用有所不同对于我来说将是非常有趣的。