首页 > 解决方案 > 如何使用取决于模板参数的字符类型定义字符串文字?

问题描述

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;

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

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

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

解决方案


有几种方法可以做到这一点,具体取决于可用的 C++ 标准版本。如果你有 C++17 可用,你可以向下滚动到Method 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};
};

方法 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 ) )

用法:

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

Coliru 现场演示

解释:

模板函数根据模板参数NarrowOrWide()返回第一个 ( char const*) 或第二个 ( wchar_t const*) 参数CharT

该宏NARROW_OR_WIDE用于避免必须同时编写窄字符串和宽字符串文字。该宏TOWSTRING只是将L前缀添加到给定的字符串文字。

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

笔记:

我会在宏名称中添加一个“唯一”前缀,类似于库的名称,以避免与其他地方定义的类似宏发生冲突。


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

C++17 终于让我们摆脱了宏,使用纯 C++ 解决方案。该解决方案使用模板参数包扩展从字符串文字初始化数组,同时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()初始化 ,同时将单个字符 ing 为所需的类型:str_arraystatic_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{} );
}

模板参数包扩展技巧是必需的,因为常量数组只能通过聚合初始化(例如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>( "äöü" );
};

Coliru 现场演示


推荐阅读