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

use*_*272 2 c++ string-literals c++11

template<typename CharType>
class StringTraits {
public:
    static const CharType NULL_CHAR = '\0';
    static constexpr CharType* WHITESPACE_STR = " ";
};

typedef StringTraits<char> AStringTraits;
typedef StringTraits<wchar_t> WStringTraits;
Run Code Online (Sandbox Code Playgroud)

我知道我可以使用模板专门化来做到这一点,但这需要一些重复(通过定义带L前缀和不带前缀的字符串文字)。

有没有更简单的方法可以在模板类中使用相同的字符串文字定义const / constexpr char / wchar_t和char * / wchar_t * ?

zet*_*t42 6

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

注意:方法1和3假定字符串文字的字符将限制为7位ASCII。这要求字符在[0..127]范围内,并且执行字符集与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};
};
Run Code Online (Sandbox Code Playgroud)

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

此答案显示了另一个变体。)

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

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 ) )
Run Code Online (Sandbox Code Playgroud)

用法:

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

Coliru现场演示

说明:

模板函数根据模板参数NarrowOrWide()返回first(char const*)或second(wchar_t const*)参数CharT

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

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

笔记:

我将在宏名称中添加一个“唯一”前缀(类似于您的库名称),以避免与在其他位置定义的相似宏发生冲突。


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

C ++ 17最终使我们摆脱了宏,使用了纯C ++解决方案。该解决方案使用模板参数包扩展从字符串文字初始化数组,同时static_cast将各个字符转换为所需的类型。

首先,我们声明一个str_array类,该类类似于std::array但为常量以null终止的字符串量身定制(例如,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
};
Run Code Online (Sandbox Code Playgroud)

到目前为止,没有什么特别的。真正的诡计是由以下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::out_of_range( "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{} );
}
Run Code Online (Sandbox Code Playgroud)

模板参数包扩权术是必需的,因为常数阵列只能通过集合初始化(如初始化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>( "äöü" );
};
Run Code Online (Sandbox Code Playgroud)

Coliru现场演示


Lou*_*ini 5

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

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

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

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

图片来源:此改进始于Mark Ransom的原始模板想法和zett42的#2 ,并借用了Chris Kushnir 的答案的一些想法,但修复了其大小限制。

此代码执行 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))
Run Code Online (Sandbox Code Playgroud)

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

我使用 Visual Studio 2019 16.7(又名_MSC_VER1927 又名 pdb 版本 14.27)中的 clang 9.0.8 和 MSVC++ 编译了此文件。我已c++2a/c++latest启用,但我认为 C++14 或 17 对于此代码来说已经足够了。

享受!