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

2023-12-30

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++17,您可以向下滚动到Method 3,我认为这是最优雅的解决方案。

Note:方法 1 和 3 假设字符串文字的字符将被限制为7 位 ASCII。这要求字符在 [0..127] 范围内并且执行字符集 https://en.cppreference.com/w/cpp/language/translation_phases与 7 位 ASCII 兼容(例如Windows-1252 https://en.wikipedia.org/wiki/Windows-1252 or UTF-8 https://en.wikipedia.org/wiki/UTF-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)

(另一种变体显示在这个答案 https://stackoverflow.com/a/52742402/7571258.)

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

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

Usage:

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

Coliru 现场演示 http://coliru.stacked-crooked.com/a/696522212d9905ee

解释:

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

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

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

Notes:

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


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

C++17终于允许我们摆脱宏并使用纯C++解决方案。该解决方案使用模板参数包 https://en.cppreference.com/w/cpp/language/parameter_pack扩展以从字符串文字初始化数组,同时static_cast将各个字符转换为所需的类型。

首先我们声明一个str_array类,类似于std::array https://en.cppreference.com/w/cpp/container/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来自字符串文字 whilestatic_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{} );
}

The 模板参数包 https://en.cppreference.com/w/cpp/language/parameter_pack需要扩展技巧,因为常量数组只能通过聚合初始化来初始化(例如const str_array<char,3> = {'a','b','c',0};),所以我们必须将字符串文字“转换”为这样的初始值设定项列表。

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

Usage:

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 现场演示 http://coliru.stacked-crooked.com/a/bda5fa86db31616a

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何定义取决于模板参数的字符类型的字符串文字? 的相关文章

随机推荐