如何使用取决于模板参数的字符类型定义字符串文字?

时间:2018-10-10 10:10:16

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

$

我知道我可以使用模板专业化来做到这一点,但这需要一些重复(通过定义带有和不带有$ ./gash.sh -r fred.txt r fred.txt $ ./gash.sh -r ./gash.sh: option requires an argument -- r $ ./gash.sh -x ./gash.sh: illegal option -- x $ ./gash.sh -dr fred.txt d r fred.txt 前缀的字符串文字)。

在模板类中是否有更简单的方法来定义具有相同 string文字的const / constexpr char / wchar_t和char * / wchar_t *?

5 个答案:

答案 0 :(得分:5)

有多种方法可以执行此操作,具体取决于C ++标准的可用版本。 如果您有可用的C ++ 17,则可以向下滚动到方法3 ,这是我认为最优雅的解决方案。

注意:方法1和3假定字符串文字的字符将被限制为 7位ASCII 。这要求字符在[0..127]范围内,并且execution character set与7位ASCII兼容(例如Windows-1252UTF-8)。否则,将这些方法使用的char值简单地转换为wchar_t并不会得出正确的结果。

方法1-聚合初始化(C ++ 03)

最简单的方法是使用聚合初始化来定义数组:

template<typename CharType>
class StringTraits {
public:
    static const CharType NULL_CHAR = '\0';
    static constexpr CharType[] WHITESPACE_STR = {'a','b','c',0};
};

方法2-模板专门化和宏(C ++ 03)

(另一个变体显示在this answer中。)

对于长字符串,聚合初始化方法可能很麻烦。为了更加舒适,我们可以结合使用模板专门化和宏:

template< typename CharT > constexpr CharT const* NarrowOrWide( char const*, wchar_t const* );
template<> constexpr char const* NarrowOrWide< char >( char const* c, wchar_t const* )       
    { return c; }
template<> constexpr wchar_t const* NarrowOrWide< wchar_t >( char const*, wchar_t const* w ) 
    { return w; }

#define TOWSTRING1(x) L##x
#define TOWSTRING(x) TOWSTRING1(x)  
#define NARROW_OR_WIDE( C, STR ) NarrowOrWide< C >( ( STR ), TOWSTRING( STR ) )

用法:

template<typename CharType>
class StringTraits {
public:
    static constexpr CharType const* WHITESPACE_STR = NARROW_OR_WIDE( CharType, " " );
};

Live Demo at Coliru

说明:

取决于模板参数NarrowOrWide(),模板函数char const*返回第一个(wchar_t const*)或第二个(CharT)参数。

NARROW_OR_WIDE用于避免同时写窄字符串和宽字符串文字。宏TOWSTRING只是在给定的字符串文字之前加上了L前缀。

当然,只有在字符范围限制为基本ASCII的情况下,宏才会起作用,但这通常就足够了。否则,可以使用NarrowOrWide()模板函数分别定义窄和宽字符串文字。

注释:

我会在宏名称(例如您的库名称)之前添加一个“唯一”前缀,以避免与在其他地方定义的相似宏发生冲突。


方法3-通过模板参数包(C ++ 17)初始化数组

C ++ 17最终使我们摆脱了宏,并使用了纯C ++解决方案。该解决方案使用template parameter pack扩展从字符串文字初始化数组,同时static_cast将各个字符设置为所需的类型。

首先,我们声明一个str_array类,该类与std::array类似,但它是为常量的空终止字符串量身定制的(例如str_array::size()返回不包含'\0'的字符数缓冲区大小)。包装类是必需的,因为不能从函数返回纯数组。它必须包装在结构或类中。

template< typename CharT, std::size_t Length >
struct str_array
{
    constexpr CharT const* c_str()              const { return data_; }
    constexpr CharT const* data()               const { return data_; }
    constexpr CharT operator[]( std::size_t i ) const { return data_[ i ]; }
    constexpr CharT const* begin()              const { return data_; }
    constexpr CharT const* end()                const { return data_ + Length; }
    constexpr std::size_t size()                const { return Length; }
    // TODO: add more members of std::basic_string

    CharT data_[ Length + 1 ];  // +1 for null-terminator
};

到目前为止,没有什么特别的。真正的骗术是通过以下str_array_cast()函数完成的,该函数从字符串文字中初始化str_array,同时static_cast将各个字符设置为所需的类型:

#include <utility>

namespace detail {
    template< typename ResT, typename SrcT >
    constexpr ResT static_cast_ascii( SrcT x )
    {
        if( !( x >= 0 && x <= 127 ) )
            throw std::runtime_error( "Character value must be in basic ASCII range (0..127)" );
        return static_cast<ResT>( x );
    }

    template< typename ResElemT, typename SrcElemT, std::size_t N, std::size_t... I >
    constexpr str_array< ResElemT, N - 1 > do_str_array_cast( const SrcElemT(&a)[N], std::index_sequence<I...> )
    {
        return { static_cast_ascii<ResElemT>( a[I] )..., 0 };
    }
} //namespace detail

template< typename ResElemT, typename SrcElemT, std::size_t N, typename Indices = std::make_index_sequence< N - 1 > >
constexpr str_array< ResElemT, N - 1 > str_array_cast( const SrcElemT(&a)[N] )
{
    return detail::do_str_array_cast< ResElemT >( a, Indices{} );
}

需要template parameter pack扩展技巧,因为常量数组只能通过聚合初始化来初始化(例如const str_array<char,3> = {'a','b','c',0};),因此我们必须将字符串文字“转换”为这样的初始化列表。

由于此答案开头给出的原因,如果任何字符超出基本ASCII范围(0..127),则代码会触发编译时错误。在某些代码页中,0..127不会映射到ASCII,因此此检查并不能提供100%的安全性。

用法:

template< typename CharT >
struct StringTraits
{
    static constexpr auto WHITESPACE_STR = str_array_cast<CharT>( "abc" );

    // Fails to compile (as intended), because characters are not basic ASCII.
    //static constexpr auto WHITESPACE_STR1 = str_array_cast<CharT>( "äöü" );
};

Live Demo at Coliru

答案 1 :(得分:2)

这是基于@ zett42的答案的替代实现。请告诉我。

#include <iostream>
#include <tuple>

#define TOWSTRING_(x) L##x
#define TOWSTRING(x) TOWSTRING_(x)  
#define MAKE_LPCTSTR(C, STR) (std::get<const C*>(std::tuple<const char*, const wchar_t*>(STR, TOWSTRING(STR))))

template<typename CharType>
class StringTraits {
public:
    static constexpr const CharType* WHITESPACE_STR = MAKE_LPCTSTR(CharType, "abc");
};

typedef StringTraits<char> AStringTraits;
typedef StringTraits<wchar_t> WStringTraits;

int main(int argc, char** argv) {
    std::cout << "Narrow string literal: " << AStringTraits::WHITESPACE_STR << std::endl;
    std::wcout << "Wide string literal  : " << WStringTraits::WHITESPACE_STR << std::endl;
    return 0;
}

答案 2 :(得分:1)

这里是对现在常见的基于模板的解决方案的改进

  • 保留C字符串的array[len] C ++类型,而不是将它们衰减为指针,这意味着您可以在结果上调用sizeof() >并获得字符串+ NUL的大小,而不是指针的大小,就像您在其中拥有原始字符串一样。

  • 即使不同编码的字符串在代码单位中具有不同的长度也可以工作(如果字符串具有非ASCII文本,这实际上得到保证)。

  • 不会引起任何运行时开销,也不会尝试/不需要在运行时进行编码转换。

信用:这种改进从Mark Ransom的原始模板构思和zett42的#2构思开始,并借鉴了Chris Kushnir's answer的构思,但修正了大小限制。

此代码执行char和wchar_t,但是将其扩展为char8_t + char16_t + char32_t

是微不足道的
// generic utility for C++ pre-processor concatenation
// - avoids a pre-processor issue if x and y have macros inside
#define _CPP_CONCAT(x, y) x ## y
#define  CPP_CONCAT(x, y) _CPP_CONCAT(x, y)

// now onto stringlit()

template<size_t SZ0, size_t SZ1>
constexpr
auto  _stringlit(char c,
                 const char     (&s0)  [SZ0],
                 const wchar_t  (&s1)  [SZ1]) -> const char(&)[SZ0] 
{
    return s0;
}

template<size_t SZ0, size_t SZ1>
constexpr
auto  _stringlit(wchar_t c,
                 const char     (&s0)  [SZ0],
                 const wchar_t  (&s1)  [SZ1]) -> const wchar_t(&)[SZ1] 
{
    return s1;
}

#define stringlit(code_unit, lit) \
    _stringlit(code_unit (), lit, CPP_CONCAT(L, lit))

在这里,我们不是在使用C ++重载,而是在每个char编码中定义一个函数,每个函数具有不同的签名。每个函数都返回具有原始边界的原始数组类型。选择适当功能的选择器是所需编码中的单个字符(该字符的值并不重要)。我们不能在模板参数中使用类型本身来进行选择,因为那样会导致重载并且返回类型冲突。此代码也可以在没有constexpr的情况下使用。注意,我们返回的是对数组的引用(在C ++中是可能的),而不是对数组的引用(在C ++中是不允许的)。此处使用尾随返回类型语法是可选的,但比其他方法更具可读性,例如const char (&stringlit(...params here...))[SZ0]等等。

我使用Visual Studio 2019 16.7(aka _MSC_VER 1927 aka pdb ver 14.27)中的clang 9.0.8和MSVC ++进行了编译。我启用了c++2a/c++latest,但我认为C ++ 14或17足以满足此代码的需要。

享受!

答案 3 :(得分:0)

我刚刚得出一个紧凑的答案,该答案与其他C ++ 17版本相似。同样,它依赖于实现定义的行为,尤其是环境字符类型。它支持将ASCII和ISO-8859-1转换为UTF-16 wchar_t,UTF-32 wchar_t,UTF-16 char16_t和UTF-32 char32_t。不支持UTF-8输入,但是更详细的转换代码是可行的。

template <typename Ch, size_t S>
constexpr auto any_string(const char (&literal)[S]) -> const array<Ch, S> {
        array<Ch, S> r = {};

        for (size_t i = 0; i < S; i++)
                r[i] = literal[i];

        return r;
}

完整示例如下:

$ cat any_string.cpp 
#include <array>
#include <fstream>

using namespace std;

template <typename Ch, size_t S>
constexpr auto any_string(const char (&literal)[S]) -> const array<Ch, S> {
        array<Ch, S> r = {};

        for (size_t i = 0; i < S; i++)
                r[i] = literal[i];

        return r;
}

int main(void)
{
    auto s = any_string<char>("Hello");
    auto ws = any_string<wchar_t>(", ");
    auto s16 = any_string<char16_t>("World");
    auto s32 = any_string<char32_t>("!\n");

    ofstream f("s.txt");
    f << s.data();
    f.close();

    wofstream wf("ws.txt");
    wf << ws.data();
    wf.close();

    basic_ofstream<char16_t> f16("s16.txt");
    f16 << s16.data();
    f16.close();

    basic_ofstream<char32_t> f32("s32.txt");
    f32 << s32.data();
    f32.close();

    return 0;
}
$ c++ -o any_string any_string.cpp -std=c++17
$ ./any_string 
$ cat s.txt ws.txt s16.txt s32.txt 
Hello, World!

答案 4 :(得分:0)

上述zett42方法2的一种变体。 具有支持所有char类型(对于可以表示为char []的文字)并保留适当的字符串文字数组类型的优点。

首先是模板功能:

template<typename CHAR_T>
constexpr
auto  LiteralChar(
    char     A,
    wchar_t  W,
    char8_t  U8,
    char16_t U16,
    char32_t U32
)   -> CHAR_T
{
         if constexpr( std::is_same_v<CHAR_T, char> )      return A;
    else if constexpr( std::is_same_v<CHAR_T, wchar_t> )   return W;
    else if constexpr( std::is_same_v<CHAR_T, char8_t> )   return U8;
    else if constexpr( std::is_same_v<CHAR_T, char16_t> )  return U16;
    else if constexpr( std::is_same_v<CHAR_T, char32_t> )  return U32;
}

template<typename CHAR_T, size_t SIZE>
constexpr
auto  LiteralStr(
    const char     (&A)  [SIZE],
    const wchar_t  (&W)  [SIZE],
    const char8_t  (&U8) [SIZE],
    const char16_t (&U16)[SIZE],
    const char32_t (&U32)[SIZE]
)   -> const CHAR_T(&)[SIZE]
{
         if constexpr( std::is_same_v<CHAR_T, char> )      return A;
    else if constexpr( std::is_same_v<CHAR_T, wchar_t> )   return W;
    else if constexpr( std::is_same_v<CHAR_T, char8_t> )   return U8;
    else if constexpr( std::is_same_v<CHAR_T, char16_t> )  return U16;
    else if constexpr( std::is_same_v<CHAR_T, char32_t> )  return U32;
}

然后宏:

#define  CMK_LC(CHAR_T, LITERAL) \
LiteralChar<CHAR_T>( LITERAL, L ## LITERAL, u8 ## LITERAL, u ## LITERAL, U ## LITERAL )

#define  CMK_LS(CHAR_T, LITERAL) \
LiteralStr<CHAR_T>( LITERAL, L ## LITERAL, u8 ## LITERAL, u ## LITERAL, U ## LITERAL )

然后使用:

template<typename CHAR_T>
class StringTraits {
public:
    struct  LC {  // literal character
        static  constexpr CHAR_T  Null  = CMK_LC(CHAR_T, '\0');
        static  constexpr CHAR_T  Space = CMK_LC(CHAR_T, ' ');
    };
    struct  LS {  // literal string
        // can't seem to avoid having to specify the size
        static  constexpr CHAR_T  Space    [2] = CMK_LS(CHAR_T, " ");
        static  constexpr CHAR_T  Ellipsis [4] = CMK_LS(CHAR_T, "...");
    };
};

auto   char_space { StringTraits<char>::LC::Space }; 
auto  wchar_space { StringTraits<wchar_t>::LC::Space };

auto   char_ellipsis { StringTraits<char>::LS::Ellipsis };     // note: const char*
auto  wchar_ellipsis { StringTraits<wchar_t>::LS::Ellipsis };  // note: const wchar_t*

auto  (& char_space_array) [4] { StringTraits<char>::LS::Ellipsis };
auto  (&wchar_space_array) [4] { StringTraits<wchar_t>::LS::Ellipsis };

? syntax to get a local copy ?

诚然,保留字符串文字数组类型的语法有点麻烦,但并不过分。 同样,仅适用于所有字符类型表示形式中具有相同代码单元编号的文字。 如果希望LiteralStr支持所有类型的所有文字,则可能需要将指针作为参数传递并返回CHAR_T *而不是CHAR_T(&)[SIZE]。不要以为可以让LiteralChar支持多字节char。

[编辑]

将Louis Semprini SIZE支持应用于LiteralStr可获得:

template<typename CHAR_T, 
    size_t SIZE_A, size_t SIZE_W, size_t SIZE_U8, size_t SIZE_U16, size_t SIZE_U32,
    size_t SIZE_R =
        std::is_same_v<CHAR_T, char>     ? SIZE_A   :
        std::is_same_v<CHAR_T, wchar_t>  ? SIZE_W   :
        std::is_same_v<CHAR_T, char8_t>  ? SIZE_U8  :
        std::is_same_v<CHAR_T, char16_t> ? SIZE_U16 :
        std::is_same_v<CHAR_T, char32_t> ? SIZE_U32 : 0
>
constexpr
auto  LiteralStr(
    const char     (&A)   [SIZE_A],
    const wchar_t  (&W)   [SIZE_W],
    const char8_t  (&U8)  [SIZE_U8],
    const char16_t (&U16) [SIZE_U16],
    const char32_t (&U32) [SIZE_U32]
)   -> const CHAR_T(&)[SIZE_R]
{
         if constexpr( std::is_same_v<CHAR_T, char> )      return A;
    else if constexpr( std::is_same_v<CHAR_T, wchar_t> )   return W;
    else if constexpr( std::is_same_v<CHAR_T, char8_t> )   return U8;
    else if constexpr( std::is_same_v<CHAR_T, char16_t> )  return U16;
    else if constexpr( std::is_same_v<CHAR_T, char32_t> )  return U32;
}

也可以使用更简单的语法来创建变量; 例如,在StringTraits :: LS中可以将其更改为constexpr auto& 所以

static  constexpr CHAR_T  Ellipsis [4] = CMK_LS(CHAR_T, "...");

成为

static  constexpr auto & Ellipsis { CMK_LS(CHAR_T, "...") };

使用CMK_LS(char,“ literal”)时,文字中的任何无效char都将转换为'?' VS 2019之前,还不确定其他编译器会做什么。