【问题标题】:C++: Can a macro expand "abc" into 'a', 'b', 'c'?C++:宏可以将“abc”扩展为“a”、“b”、“c”吗?
【发布时间】:2011-06-02 18:03:40
【问题描述】:

我编写了一个可变参数模板,它接受可变数量的char 参数,即

template <char... Chars>
struct Foo;

我只是想知道是否有任何宏技巧可以让我使用类似于以下的语法来实例化它:

Foo<"abc">

Foo<SOME_MACRO("abc")>

Foo<SOME_MACRO(abc)>

等等

基本上,任何能阻止你单独写字符的东西,就像这样

Foo<'a', 'b', 'c'>

这对我来说不是什么大问题,因为它只是一个玩具程序,但我想我还是会问。

【问题讨论】:

  • "abc"'a', 'b', 'c', '\0' 基本相同,除了指针内容。
  • 过去的情况是,如果模板是通过 char* 参数化的,则无法使用原始 C 字符串在 C++ 中实例化模板。他们在 C++0x 中解决了这个问题吗?如果是这样,我想我有办法正确地进行此扩展。
  • @Ignacio:我知道,但你不能为char... 模板参数写"abc"。 @templatetypedef:该模板没有在char* 上进行参数化,它是char... 上的可变参数模板
  • @Peter Alexander:是的,是的...但是您不能构建一个辅助模板类,该类在导出元组的 char* 上参数化,然后创建一个实例化该辅助模板的宏,然后提取元组会输入吗?我就是这么想的。
  • 我认为,在 C++0x n3225 中,规范还允许 constexpr char index(char const *x, int n) { return x[n]; }。然后你可以说int x[index("\x4\x5", 1)]; 来创建一个int[5] 例如。这就是函数调用替换。

标签: c++ string c++11 c-preprocessor


【解决方案1】:

我今天创建了一个,并在 GCC4.6.0 上进行了测试。

#include <iostream>

#define E(L,I) \
  (I < sizeof(L)) ? L[I] : 0

#define STR(X, L)                                                       \
  typename Expand<X,                                                    \
                  cstring<E(L,0),E(L,1),E(L,2),E(L,3),E(L,4), E(L,5),   \
                          E(L,6),E(L,7),E(L,8),E(L,9),E(L,10), E(L,11), \
                          E(L,12),E(L,13),E(L,14),E(L,15),E(L,16), E(L,17)> \
                  cstring<>, sizeof L-1>::type

#define CSTR(L) STR(cstring, L)

template<char ...C> struct cstring { };

template<template<char...> class P, typename S, typename R, int N>
struct Expand;

template<template<char...> class P, char S1, char ...S, char ...R, int N>
struct Expand<P, cstring<S1, S...>, cstring<R...>, N> :
  Expand<P, cstring<S...>, cstring<R..., S1>, N-1>{ };

template<template<char...> class P, char S1, char ...S, char ...R>
struct Expand<P, cstring<S1, S...>, cstring<R...>, 0> {
  typedef P<R...> type;
};

一些测试

template<char ...S> 
struct Test {
  static void print() {
    char x[] = { S... };
    std::cout << sizeof...(S) << std::endl;
    std::cout << x << std::endl;
  }
};

template<char ...C>
void process(cstring<C...>) {
  /* process C, possibly at compile time */
}

int main() {
  typedef STR(Test, "Hello folks") type;
  type::print();

  process(CSTR("Hi guys")());
}

因此,虽然您没有得到 'a', 'b', 'c',但您仍然会得到编译时字符串。

【讨论】:

  • 很有趣,但我是否正确地说这只能处理长度为 18 个字符串?
  • @peter,是的。但你可以添加更多的 E'es。所以不是真正的限制。参考文献boost.pp
  • @litb: 是的,但我用例中的字符串很容易变成数千个字符:-)
  • @Peter 它不支持它们。但即使它会:我试图想办法做到这一点,但我认为没有宏是不可能的。将a, b, c 传递给模板是一个语法问题,所以无论如何你都需要一个宏。您可以在编译时使用带有constexpr 函数的用户定义文字处理字符串,但该函数的返回类型不能依赖于字符串文字的内容.仅仅是回报的价值就可以了。至少是 AFAICS。
  • @Peter:数千个字符?甚至可以编译。我敢肯定,在你得到这么大的可变参数模板之前,你会达到一些内部编译器限制。
【解决方案2】:

基于 Sylvain Defresne 上述响应的解决方案在 C++11 中是可能的:

#include <boost/preprocessor/repetition/repeat.hpp>
#include <boost/preprocessor/punctuation/comma_if.hpp>

template <unsigned int N>
constexpr char get_ch (char const (&s) [N], unsigned int i)
{
    return i >= N ? '\0' : s[i];
}

#define STRING_TO_CHARS_EXTRACT(z, n, data) \
        BOOST_PP_COMMA_IF(n) get_ch(data, n)

#define STRING_TO_CHARS(STRLEN, STR)  \
        BOOST_PP_REPEAT(STRLEN, STRING_TO_CHARS_EXTRACT, STR)

// Foo <STRING_TO_CHARS(3, "abc")>
//   expands to
// Foo <'a', 'b', 'c'>

此外,如果所讨论的模板能够处理多个终止 '\0' 字符,我们可以放宽长度要求以支持最大长度:

#define STRING_TO_CHARS_ANY(STR) \
        STRING_TO_CHARS(100, STR)

// Foo <STRING_TO_CHARS_ANY("abc")>
//   expands to
// Foo <'a', 'b', 'c', '\0', '\0', ...>

以上示例在 clang++ (3.2) 和 g++ (4.8.0) 上正确编译。

【讨论】:

    【解决方案3】:

    有很多试验,但我认为它最终注定要失败。

    要了解原因,需要了解预处理器的工作原理。预处理器的输入可以被认为是一个流。此流首先在 preprocessing-tokens 中进行转换(列表在 The C++ Programming Language, 3rd Edition, Annexe A Grammar, page 795)

    在这些标记上,预处理器只能应用非常有限数量的操作,除了 digrams/trigrams 的东西,这相当于:

    • 文件包含(用于头指令),据我所知,这可能不会出现在宏中
    • 宏替换(这是非常复杂的东西:p)
    • #:将标记转换为 string-literal 标记(用引号括起来)
    • ##:连接两个标记

    就是这样。

    • 没有预处理器指令可以将一个标记拆分为多个标记:这是宏替换,这意味着实际上首先定义了一个宏
    • 没有预处理器指令可以将 string-literal 转换为常规标记(删除引号),然后可以进行宏替换。

    因此,我认为这是不可能的(在 C++03 或 C++0x 中),尽管可能(可能)对此有编译器特定的扩展。

    【讨论】:

    • 这很不幸,但我认为你是对的。这种操作需要比 C++ 预处理器更强大的东西。
    • # 在这里很有用。它将允许FOO(a,b,c) 扩展为'a','b','c'。使用这两个宏:#define asChar(x) #x[0]#define FOO(x,y,z) asChar(asChar(x)),asChar(asChar(y)),asChar(asChar(z)) 经过测试,它可以工作。可惜这个简单的版本被硬编码为三个字符。 clang3.3 和 g++-4.6,但我不认为它使用了太花哨的东西。如果"sdlkfj"[0] 在编译时计算为's',那么它应该可以在任何编译器上工作。
    【解决方案4】:

    基于上述user1653543的解决方案。

    一些模板魔法:

    template <unsigned int N>
    constexpr char getch (char const (&s) [N], unsigned int i)
    {
        return i >= N ? '\0' : s[i];
    }
    
    template<char ... Cs>
    struct split_helper;
    
    template<char C, char ... Cs>
    struct split_helper<C, Cs...>
    {
        typedef push_front_t<typename split_helper<Cs...>::type, char_<C>> type;
    };
    
    template<char ... Cs>
    struct split_helper<'\0', Cs...>
    {
        typedef std::integer_sequence<char> type;
    };
    
    template<char ... Cs>
    using split_helper_t = typename split_helper<Cs...>::type;
    

    一些 PP 魔法:

    #define SPLIT_CHARS_EXTRACT(z, n, data) \
        BOOST_PP_COMMA_IF(n) getch(data, n)
    
    #define STRING_N(n, str) \
        split_helper_t<BOOST_PP_REPEAT(n, SPLIT_CHARS_EXTRACT, str)>
    
    #define STRING(str) STRING_N(BOOST_PP_LIMIT_REPEAT, str)
    

    split_helper 只是减少尾随零的助手。现在STRING("Hello") 是一个类型化的编译时字符序列(std::integer_sequence&lt;char, 'H', 'e', 'l', 'l', 'o'&gt;)。字符串常量的长度最多为BOOST_PP_LIMIT_REPEAT 个字符。

    作业:实现push_front_tc_str 以获取std::integer_sequence&lt;char, ...&gt; 的空终止字符串。 (不过,您可以尝试使用 Boost.MPL)

    【讨论】:

    • 这是迄今为止最好的解决方案。我不知道,为什么它在页面上如此低。 +1
    【解决方案5】:

    这曾经在msvc的早期版本中工作,我不知道它是否仍然有效:

    #define CHAR_SPLIT(...) #@__VA_ARGS__
    

    【讨论】:

    • 对不起,那我没主意了:(
    【解决方案6】:

    很遗憾,我认为这是无法做到的。 Boost.Preprocessor 提供了您可以从预处理器中获得的最佳效果,尤其是通过它的数据类型:

    • array :语法为 (3, (a, b, c))
    • list :语法为 (a, (b, (c, BOOST_PP_NIL)))
    • sequence :语法为 (a)(b)(c)
    • tuple :语法为 (a, b, c)

    从这些类型中的任何一种,您都可以轻松创建一个宏,该宏将构建一个逗号分隔的单引号括起来的项目列表(例如参见BOOST_PP_SEQ_ENUM),但我相信这个宏的输入必须是一个这些类型中的所有类型都需要单独键入字符。

    【讨论】:

    • 我在 BOOST_PP_SEQ_ENUM 上停了下来,但是如何在其中添加引号?
    【解决方案7】:

    根据我上面讨论的内容,以下可怕的模板黑客可能足以解决这个问题。我还没有测试过这个(对不起!),但我很确定它或接近它的东西可能会起作用。

    第一步是构建一个只包含一个字符元组的模板类:

    template <char... Chars> class CharTuple {};
    

    现在,让我们构建一个可以将 C 样式字符串转换为 CharTuple 的适配器。为此,我们需要以下辅助类,它本质上是一个 LISP 风格的元组 cons:

    template <typename Tuple, char ch> class Cons;
    template <char... Chars, char ch> class Cons<CharTuple<Chars... ch>> {
        typedef CharTuple<ch, Chars...> type;
    }
    

    假设我们有一个 meta-if 语句:

    template <bool Condition, typename TrueType, typename FalseType> class If {
        typedef typename TrueType::type type;
    };
    template <typename TrueType, typename FalseType> class If<False> {
        typedef typename FalseType::type type;
    };
    

    那么下面应该可以让你将 C 风格的字符串转换为元组:

    template <typename T> class Identity {
        typedef T type;
    };
    
    template <char* str> class StringToChars {
        typedef typename If<*str == '\0', Identity<CharTuple<>>,
                            Cons<*str, typename StringToChars<str + 1>::type>>::type type;
    };
    

    现在您可以将 C 样式的字符串转换为字符元组,您可以通过这种类型汇集输入字符串以恢复元组。不过,我们需要做更多的机器来完成这项工作。 TMP 不好玩吗? :-)

    第一步是获取你的原始代码:

    template <char... Chars> class Foo { /* ... */ };
    

    并使用一些模板专业化将其转换为

    template <typename> class FooImpl;
    tempalte <char... Chars> class FooImpl<CharTuple<Chars...>> { /* ... */ };
    

    这只是另一层间接;仅此而已。

    最后,你应该能够做到这一点:

    template <char* str> class Foo {
        typedef typename FooImpl<typename StringToChars<str>::type>::type type;
    };
    

    我真的希望这有效。如果不是,我仍然认为这值得发布,因为它可能 ε-接近有效答案。 :-)

    【讨论】:

    • 我不相信你可以在编译时取消引用str,但我会试一试。
    • 这是一个有效的观点;我以前从未真正尝试过这样做。您是否可以在空字符串上使用该类型的模板特化?或者这也不能正常工作?
    • 另一个想法(抱歉,如果这开始变得烦人) - 你能在编译时以某种方式反省字符串的长度吗?如果是这样,您可以通过从字符串的长度倒数到零来实现 StringToChars。使用宏可能会使这更容易。使用新的 constexpr 关键字这可能是可行的,但我对 C++0x 的理解还不够强,无法知道这是否可能。
    • 获取数组的大小很容易,但你也不能在编译时对字符串使用[] 运算符。
    • 但是你不能定义一个接口给你一个ptr到chars的元组吗?
    【解决方案8】:

    在 C++14 中,这可以通过使用立即调用的 lambda 和静态成员函数来完成,类似于 BOOST_HANA_STRING

    #include <utility>
    
    template <char... Cs>
    struct my_string {};
    
    template <typename T, std::size_t... Is>
    constexpr auto as_chars_impl(std::index_sequence<Is...>) {
        return my_string<T::str()[Is]...>{};
    }
    
    template <typename T>
    constexpr auto as_chars() {
        return as_chars_impl<T>(
            std::make_index_sequence<sizeof(T::str())-1>{});
    }
    
    #define STR(literal)                                        \
        []{                                                     \
            struct literal_to_chars {                           \
                static constexpr decltype(auto) str() {         \
                    return literal;                             \
                }                                               \
            };                                                  \
            return as_chars<literal_to_chars>();                \
        }()
    

    Live on Godbolt

    在 C++17 之前,STR("some literal") 返回的对象不能是 constexpr,因为 lambda 不能是 constexpr。 在 C++20 之前,您不能只写 decltype(STR("some literal")),因为未计算的上下文中不允许使用 lambda。

    【讨论】:

    • @AlexisWilke 这是故意的。 SomeType::some_invalid_member 是让编译器输出类型名称的众多方法之一。我一般选择_作为无效成员。
    猜你喜欢
    • 2016-10-24
    • 2011-05-30
    • 2022-11-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-28
    相关资源
    最近更新 更多