在C++中方便地声明编译时字符串

voi*_*ter 131 c++ string metaprogramming user-defined-literals c++11

能够在C++编译时创建和操作字符串有几个有用的应用程序.尽管可以在C++中创建编译时字符串,但是该过程非常麻烦,因为字符串需要声明为可变字符序列,例如

using str = sequence<'H', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!'>;
Run Code Online (Sandbox Code Playgroud)

诸如字符串连接,子字符串提取等许多操作可以很容易地实现为对字符序列的操作.是否可以更方便地声明编译时字符串?如果没有,是否有一个提案可以方便地声明编译时字符串?

为什么现有方法失败

理想情况下,我们希望能够如下声明编译时字符串:

// Approach 1
using str1 = sequence<"Hello, world!">;
Run Code Online (Sandbox Code Playgroud)

或者,使用用户定义的文字,

// Approach 2
constexpr auto str2 = "Hello, world!"_s;
Run Code Online (Sandbox Code Playgroud)

哪里decltype(str2)有一个constexpr构造函数.方法1的混乱版本可以实现,利用您可以执行以下操作的事实:

template <unsigned Size, const char Array[Size]>
struct foo;
Run Code Online (Sandbox Code Playgroud)

但是,数组需要有外部链接,所以要使方法1起作用,我们必须编写如下内容:

/* Implementation of array to sequence goes here. */

constexpr const char str[] = "Hello, world!";

int main()
{
    using s = string<13, str>;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

不用说,这非常不方便.方法2实际上是不可能实现的.如果我们要声明一个(constexpr)文字运算符,那么我们如何指定返回类型?由于我们需要运算符返回可变字符序列,因此我们需要使用该const char*参数来指定返回类型:

constexpr auto
operator"" _s(const char* s, size_t n) -> /* Some metafunction using `s` */
Run Code Online (Sandbox Code Playgroud)

这会导致编译错误,因为s不是a constexpr.尝试通过执行以下操作解决此问题并没有多大帮助.

template <char... Ts>
constexpr sequence<Ts...> operator"" _s() { return {}; }
Run Code Online (Sandbox Code Playgroud)

该标准规定此特定的文字运算符形式保留给整数和浮点类型.虽然123_s会起作用,abc_s但不会.如果我们完全抛弃用户定义的文字,并使用常规constexpr函数怎么办?

template <unsigned Size>
constexpr auto
string(const char (&array)[Size]) -> /* Some metafunction using `array` */
Run Code Online (Sandbox Code Playgroud)

和以前一样,我们遇到的问题是,数组(现在是constexpr函数的参数)本身不再是constexpr类型.

我相信应该可以定义一个C预处理器宏,它接受一个字符串和字符串的大小作为参数,并返回一个由字符串中的字符组成的序列(使用BOOST_PP_FOR,字符串化,数组下标等).但是,我没有时间(或足够的兴趣)来实现这样的宏=)

How*_*ant 121

我没有看到任何与Scott Schurrstr_constC++ Now 2012 呈现的优雅相匹配的东西.它确实需要constexpr.

以下是如何使用它以及它可以做什么:

int
main()
{
    constexpr str_const my_string = "Hello, world!";
    static_assert(my_string.size() == 13, "");
    static_assert(my_string[4] == 'o', "");
    constexpr str_const my_other_string = my_string;
    static_assert(my_string == my_other_string, "");
    constexpr str_const world(my_string, 7, 5);
    static_assert(world == "world", "");
//  constexpr char x = world[5]; // Does not compile because index is out of range!
}
Run Code Online (Sandbox Code Playgroud)

它没有比编译时范围检查更冷!

使用和实现都没有宏.字符串大小没有人为限制.我在这里发布了实现,但我尊重Scott隐含的版权.实施是在他的演示文稿的单个幻灯片上链接到上面.

  • 这是一段很好的代码.但是,与使用字符序列作为模板参数声明的字符串相比,这种方法仍然存在缺陷:str_const是常量值,而不是类型,因此阻止使用大量元编程习惯用法. (5认同)
  • 创建新constexpr字符串的操作(如字符串连接和子字符串提取)是否可以使用此方法?也许使用两个constexpr-string类(一个基于`str_const`,另一个基于`sequence`),这可能是可能的.用户将使用`str_const`来初始化字符串,但是后续创建新字符串的操作将返回`sequence`对象. (3认同)
  • 我不同意这种热情......不使用模板元函数 - *非常*烦人,因为 constexpr 函数应在运行时可调用的愚蠢妥协 - 没有真正的串联,需要定义一个 char 数组(头文件中丑陋) -尽管由于前面提到的 constexpr 妥协,大多数无宏解决方案都是如此 - 并且范围检查并没有给我留下太多印象,因为即使是低级的 constexpr const char * 也有这样的功能。我推出了自己的参数包字符串,它也可以由文字(使用元函数)制成,但需要数组定义。 (2认同)
  • 我认为答案中提供的示例不正确。Scotts 演示文稿第 29 页的代码没有相等运算符 - 因此您不能执行 static_assert(my_string == my_other_string, ""); (2认同)
  • @ user975326:我刚刚查看了我的实现,看起来我添加了一个`constexpr operator ==`.抱歉.斯科特的演讲应该让你开始学习如何做到这一点.在C++ 14中比在C++ 11中容易得多.我甚至不想在C++ 11中尝试.请参阅Scott最新的"constexpr"会谈:https://www.youtube.com/user/CppCon (2认同)

小智 39

我相信应该可以定义一个C预处理器宏,它接受一个字符串和字符串的大小作为参数,并返回一个由字符串中的字符组成的序列(使用BOOST_PP_FOR,字符串化,数组下标等).但是,我没有时间(或足够的兴趣)来实现这样的宏

可以在不依赖boost的情况下实现这一点,使用非常简单的宏和一些C++ 11特性:

  1. lambdas variadic
  2. 模板
  3. 广义常量表达式
  4. 非静态数据成员初始值设定项
  5. 统一初始化

(这里不严格要求后两者)

  1. 我们需要能够使用用户提供的从0到N的标记来实例化一个可变参数模板 - 这个工具也很有用,例如将元组扩展为可变参数模板函数的参数(参见问题: 如何将元组扩展为可变参数模板函数的参数?
    )拆包"的元组来调用匹配函数指针)

    namespace  variadic_toolbox
    {
        template<unsigned  count, 
            template<unsigned...> class  meta_functor, unsigned...  indices>
        struct  apply_range
        {
            typedef  typename apply_range<count-1, meta_functor, count-1, indices...>::result  result;
        };
    
        template<template<unsigned...> class  meta_functor, unsigned...  indices>
        struct  apply_range<0, meta_functor, indices...>
        {
            typedef  typename meta_functor<indices...>::result  result;
        };
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 然后使用非类型参数char定义名为string的可变参数模板:

    namespace  compile_time
    {
        template<char...  str>
        struct  string
        {
            static  constexpr  const char  chars[sizeof...(str)+1] = {str..., '\0'};
        };
    
        template<char...  str>
        constexpr  const char  string<str...>::chars[sizeof...(str)+1];
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 现在最有趣的部分 - 将字符文字传递给字符串模板:

    namespace  compile_time
    {
        template<typename  lambda_str_type>
        struct  string_builder
        {
            template<unsigned... indices>
            struct  produce
            {
                typedef  string<lambda_str_type{}.chars[indices]...>  result;
            };
        };
    }
    
    #define  CSTRING(string_literal)                                                        \
        []{                                                                                 \
            struct  constexpr_string_type { const char * chars = string_literal; };         \
            return  variadic_toolbox::apply_range<sizeof(string_literal)-1,                 \
                compile_time::string_builder<constexpr_string_type>::produce>::result{};    \
        }()
    
    Run Code Online (Sandbox Code Playgroud)

一个简单的串联演示显示了用法:

    namespace  compile_time
    {
        template<char...  str0, char...  str1>
        string<str0..., str1...>  operator*(string<str0...>, string<str1...>)
        {
            return  {};
        }
    }

    int main()
    {
        auto  str0 = CSTRING("hello");
        auto  str1 = CSTRING(" world");

        std::cout << "runtime concat: " <<  str_hello.chars  << str_world.chars  << "\n <=> \n";
        std::cout << "compile concat: " <<  (str_hello * str_world).chars  <<  std::endl;
    }
Run Code Online (Sandbox Code Playgroud)

https://ideone.com/8Ft2xu

  • 这太简单了,我仍然无法相信它有效。+1!一件事:你不应该使用 size_t 而不是 unsigned 吗? (2认同)
  • 那么使用 `operator+` 代替 `operator*` 怎么样?`(str_hello + str_world)` (2认同)

dyp*_*dyp 20

编辑:正如Howard Hinnant(以及我对OP的评论中的某些内容)所指出的那样,您可能不需要将字符串的每个字符作为单个模板参数的类型.如果你确实需要这个,下面有一个无宏的解决方案.

在编译时尝试使用字符串时我发现了一个技巧.它需要引入除"模板字符串"之外的其他类型,但在函数内,您可以限制此类型的范围.

它不使用宏,而是使用一些C++ 11特性.

#include <iostream>

// helper function
constexpr unsigned c_strlen( char const* str, unsigned count = 0 )
{
    return ('\0' == str[0]) ? count : c_strlen(str+1, count+1);
}

// helper "function" struct
template < char t_c, char... tt_c >
struct rec_print
{
    static void print()
    {
        std::cout << t_c;
        rec_print < tt_c... > :: print ();
    }
};
    template < char t_c >
    struct rec_print < t_c >
    {
        static void print() { std::cout << t_c; }
    };


// destination "template string" type
template < char... tt_c >
struct exploded_string
{
    static void print()
    {
        rec_print < tt_c... > :: print();
    }
};

// struct to explode a `char const*` to an `exploded_string` type
template < typename T_StrProvider, unsigned t_len, char... tt_c >
struct explode_impl
{
    using result =
        typename explode_impl < T_StrProvider, t_len-1,
                                T_StrProvider::str()[t_len-1],
                                tt_c... > :: result;
};

    template < typename T_StrProvider, char... tt_c >
    struct explode_impl < T_StrProvider, 0, tt_c... >
    {
         using result = exploded_string < tt_c... >;
    };

// syntactical sugar
template < typename T_StrProvider >
using explode =
    typename explode_impl < T_StrProvider,
                            c_strlen(T_StrProvider::str()) > :: result;


int main()
{
    // the trick is to introduce a type which provides the string, rather than
    // storing the string itself
    struct my_str_provider
    {
        constexpr static char const* str() { return "hello world"; }
    };

    auto my_str = explode < my_str_provider >{};    // as a variable
    using My_Str = explode < my_str_provider >;    // as a type

    my_str.print();
}
Run Code Online (Sandbox Code Playgroud)

  • 我认为更简单的选择是执行 `char str[] = {ttc...}; 而不是递归打印机。std::cout &lt;&lt; str &lt;&lt; std::endl;` (2认同)

Yan*_*kes 9

如果你不想使用Boost解决方案,你可以创建一个类似的东西:

#define MACRO_GET_1(str, i) \
    (sizeof(str) > (i) ? str[(i)] : 0)

#define MACRO_GET_4(str, i) \
    MACRO_GET_1(str, i+0),  \
    MACRO_GET_1(str, i+1),  \
    MACRO_GET_1(str, i+2),  \
    MACRO_GET_1(str, i+3)

#define MACRO_GET_16(str, i) \
    MACRO_GET_4(str, i+0),   \
    MACRO_GET_4(str, i+4),   \
    MACRO_GET_4(str, i+8),   \
    MACRO_GET_4(str, i+12)

#define MACRO_GET_64(str, i) \
    MACRO_GET_16(str, i+0),  \
    MACRO_GET_16(str, i+16), \
    MACRO_GET_16(str, i+32), \
    MACRO_GET_16(str, i+48)

#define MACRO_GET_STR(str) MACRO_GET_64(str, 0), 0 //guard for longer strings

using seq = sequence<MACRO_GET_STR("Hello world!")>;
Run Code Online (Sandbox Code Playgroud)

唯一的问题是64个字符的固定大小(加上额外的零).但它可以根据您的需要轻松更改.


Evg*_*yuk 6

我相信应该可以定义一个C预处理器宏,它接受一个字符串和字符串的大小作为参数,并返回一个由字符串中的字符组成的序列(使用BOOST_PP_FOR,字符串化,数组下标等)

有文章: Abel Sinkovics和Dave Abrahams的C++模板元程序中使用字符串.

它比使用宏+ BOOST_PP_REPEAT的想法有一些改进- 它不需要将显式大小传递给宏.简而言之,它基于字符串大小的固定上限和"字符串溢出保护":

template <int N>
constexpr char at(char const(&s)[N], int i)
{
    return i >= N ? '\0' : s[i];
}
Run Code Online (Sandbox Code Playgroud)

加上条件boost :: mpl :: push_back.


我改变了我对Yankes解决方案的接受答案,因为它解决了这个特定的问题,并且在没有使用constexpr或复杂的预处理器代码的情况下做得很优雅.

如果您接受尾随零,手写宏循环,扩展宏中2x重复字符串,并且没有Boost - 那么我同意 - 它更好.虽然,Boost只有三行:

现场演示

#include <boost/preprocessor/repetition/repeat.hpp>
#define GET_STR_AUX(_, i, str) (sizeof(str) > (i) ? str[(i)] : 0),
#define GET_STR(str) BOOST_PP_REPEAT(64,GET_STR_AUX,str) 0
Run Code Online (Sandbox Code Playgroud)


Áti*_*ves 5

一位同事要求我在编译时连接内存中的字符串。它还包括在编译时实例化单个字符串。完整的代码清单在这里:

//Arrange strings contiguously in memory at compile-time from string literals.
//All free functions prefixed with "my" to faciliate grepping the symbol tree
//(none of them should show up).

#include <iostream>

using std::size_t;

//wrapper for const char* to "allocate" space for it at compile-time
template<size_t N>
struct String {
    //C arrays can only be initialised with a comma-delimited list
    //of values in curly braces. Good thing the compiler expands
    //parameter packs into comma-delimited lists. Now we just have
    //to get a parameter pack of char into the constructor.
    template<typename... Args>
    constexpr String(Args... args):_str{ args... } { }
    const char _str[N];
};

//takes variadic number of chars, creates String object from it.
//i.e. myMakeStringFromChars('f', 'o', 'o', '\0') -> String<4>::_str = "foo"
template<typename... Args>
constexpr auto myMakeStringFromChars(Args... args) -> String<sizeof...(Args)> {
    return String<sizeof...(args)>(args...);
}

//This struct is here just because the iteration is going up instead of
//down. The solution was to mix traditional template metaprogramming
//with constexpr to be able to terminate the recursion since the template
//parameter N is needed in order to return the right-sized String<N>.
//This class exists only to dispatch on the recursion being finished or not.
//The default below continues recursion.
template<bool TERMINATE>
struct RecurseOrStop {
    template<size_t N, size_t I, typename... Args>
    static constexpr String<N> recurseOrStop(const char* str, Args... args);
};

//Specialisation to terminate recursion when all characters have been
//stripped from the string and converted to a variadic template parameter pack.
template<>
struct RecurseOrStop<true> {
    template<size_t N, size_t I, typename... Args>
    static constexpr String<N> recurseOrStop(const char* str, Args... args);
};

//Actual function to recurse over the string and turn it into a variadic
//parameter list of characters.
//Named differently to avoid infinite recursion.
template<size_t N, size_t I = 0, typename... Args>
constexpr String<N> myRecurseOrStop(const char* str, Args... args) {
    //template needed after :: since the compiler needs to distinguish
    //between recurseOrStop being a function template with 2 paramaters
    //or an enum being compared to N (recurseOrStop < N)
    return RecurseOrStop<I == N>::template recurseOrStop<N, I>(str, args...);
}

//implementation of the declaration above
//add a character to the end of the parameter pack and recurse to next character.
template<bool TERMINATE>
template<size_t N, size_t I, typename... Args>
constexpr String<N> RecurseOrStop<TERMINATE>::recurseOrStop(const char* str,
                                                            Args... args) {
    return myRecurseOrStop<N, I + 1>(str, args..., str[I]);
}

//implementation of the declaration above
//terminate recursion and construct string from full list of characters.
template<size_t N, size_t I, typename... Args>
constexpr String<N> RecurseOrStop<true>::recurseOrStop(const char* str,
                                                       Args... args) {
    return myMakeStringFromChars(args...);
}

//takes a compile-time static string literal and returns String<N> from it
//this happens by transforming the string literal into a variadic paramater
//pack of char.
//i.e. myMakeString("foo") -> calls myMakeStringFromChars('f', 'o', 'o', '\0');
template<size_t N>
constexpr String<N> myMakeString(const char (&str)[N]) {
    return myRecurseOrStop<N>(str);
}

//Simple tuple implementation. The only reason std::tuple isn't being used
//is because its only constexpr constructor is the default constructor.
//We need a constexpr constructor to be able to do compile-time shenanigans,
//and it's easier to roll our own tuple than to edit the standard library code.

//use MyTupleLeaf to construct MyTuple and make sure the order in memory
//is the same as the order of the variadic parameter pack passed to MyTuple.
template<typename T>
struct MyTupleLeaf {
    constexpr MyTupleLeaf(T value):_value(value) { }
    T _value;
};

//Use MyTupleLeaf implementation to define MyTuple.
//Won't work if used with 2 String<> objects of the same size but this
//is just a toy implementation anyway. Multiple inheritance guarantees
//data in the same order in memory as the variadic parameters.
template<typename... Args>
struct MyTuple: public MyTupleLeaf<Args>... {
    constexpr MyTuple(Args... args):MyTupleLeaf<Args>(args)... { }
};

//Helper function akin to std::make_tuple. Needed since functions can deduce
//types from parameter values, but classes can't.
template<typename... Args>
constexpr MyTuple<Args...> myMakeTuple(Args... args) {
    return MyTuple<Args...>(args...);
}

//Takes a variadic list of string literals and returns a tuple of String<> objects.
//These will be contiguous in memory. Trailing '\0' adds 1 to the size of each string.
//i.e. ("foo", "foobar") -> (const char (&arg1)[4], const char (&arg2)[7]) params ->
//                       ->  MyTuple<String<4>, String<7>> return value
template<size_t... Sizes>
constexpr auto myMakeStrings(const char (&...args)[Sizes]) -> MyTuple<String<Sizes>...> {
    //expands into myMakeTuple(myMakeString(arg1), myMakeString(arg2), ...)
    return myMakeTuple(myMakeString(args)...);
}

//Prints tuple of strings
template<typename T> //just to avoid typing the tuple type of the strings param
void printStrings(const T& strings) {
    //No std::get or any other helpers for MyTuple, so intead just cast it to
    //const char* to explore its layout in memory. We could add iterators to
    //myTuple and do "for(auto data: strings)" for ease of use, but the whole
    //point of this exercise is the memory layout and nothing makes that clearer
    //than the ugly cast below.
    const char* const chars = reinterpret_cast<const char*>(&strings);
    std::cout << "Printing strings of total size " << sizeof(strings);
    std::cout << " bytes:\n";
    std::cout << "-------------------------------\n";

    for(size_t i = 0; i < sizeof(strings); ++i) {
        chars[i] == '\0' ? std::cout << "\n" : std::cout << chars[i];
    }

    std::cout << "-------------------------------\n";
    std::cout << "\n\n";
}

int main() {
    {
        constexpr auto strings = myMakeStrings("foo", "foobar",
                                               "strings at compile time");
        printStrings(strings);
    }

    {
        constexpr auto strings = myMakeStrings("Some more strings",
                                               "just to show Jeff to not try",
                                               "to challenge C++11 again :P",
                                               "with more",
                                               "to show this is variadic");
        printStrings(strings);
    }

    std::cout << "Running 'objdump -t |grep my' should show that none of the\n";
    std::cout << "functions defined in this file (except printStrings()) are in\n";
    std::cout << "the executable. All computations are done by the compiler at\n";
    std::cout << "compile-time. printStrings() executes at run-time.\n";
}
Run Code Online (Sandbox Code Playgroud)

  • 确实如此。我再次尝试使用 gcc 4.9,它仍然做同样的事情。我一直认为这是编译器愚蠢。直到昨天我才想尝试不同的编译器。使用 clang,字节 movs 根本不存在。使用 gcc, -Os 也可以摆脱它们,但 -O3 做同样的事情。 (2认同)

kac*_*cey 5

这是为每个传递的编译时字符串创建 std::tuple<char...> 的简洁 C++14 解决方案。

#include <tuple>
#include <utility>


namespace detail {
        template <std::size_t ... indices>
        decltype(auto) build_string(const char * str, std::index_sequence<indices...>) {
                return std::make_tuple(str[indices]...);
        }
}

template <std::size_t N>
constexpr decltype(auto) make_string(const char(&str)[N]) {
        return detail::build_string(str, std::make_index_sequence<N>());
}

auto HelloStrObject = make_string("hello");
Run Code Online (Sandbox Code Playgroud)

这是创建一个独特的编译时类型的一个,从另一个宏帖子中删减。

#include <utility>

template <char ... Chars>
struct String {};

template <typename Str, std::size_t ... indices>
decltype(auto) build_string(std::index_sequence<indices...>) {
        return String<Str().chars[indices]...>();
}

#define make_string(str) []{\
        struct Str { const char * chars = str; };\
        return build_string<Str>(std::make_index_sequence<sizeof(str)>());\
}()

auto HelloStrObject = make_string("hello");
Run Code Online (Sandbox Code Playgroud)

用户定义的文字还不能用于这一点真的太糟糕了。


Nic*_*man 5

似乎没有人喜欢我的另一个答案:-<。所以在这里我展示了如何将 str_const 转换为真实类型:

#include <iostream>
#include <utility>

// constexpr string with const member functions
class str_const { 
private:
    const char* const p_;
    const std::size_t sz_;
public:

    template<std::size_t N>
    constexpr str_const(const char(&a)[N]) : // ctor
    p_(a), sz_(N-1) {}

    constexpr char operator[](std::size_t n) const { 
        return n < sz_ ? p_[n] :
        throw std::out_of_range("");
    }

    constexpr std::size_t size() const { return sz_; } // size()
};


template <char... letters>
struct string_t{
    static char const * c_str() {
        static constexpr char string[]={letters...,'\0'};
        return string;
    }
};

template<str_const const& str,std::size_t... I>
auto constexpr expand(std::index_sequence<I...>){
    return string_t<str[I]...>{};
}

template<str_const const& str>
using string_const_to_type = decltype(expand<str>(std::make_index_sequence<str.size()>{}));

constexpr str_const hello{"Hello World"};
using hello_t = string_const_to_type<hello>;

int main()
{
//    char c = hello_t{};        // Compile error to print type
    std::cout << hello_t::c_str();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

使用 clang++ -stdlib=libc++ -std=c++14 (clang 3.7) 编译