模板、字符串字面量和UNICODE

5

新消息:感谢所有帮助我的人!下面标有答案,并且我在问题下方(请参见)扩展了答案的一个可行版本:


我似乎经常遇到这种情况(在更新我们的字符串工具库时):

我需要一种适用于char和wchar_t的模板,使用各种字符串字面量。目前我发现这很具有挑战性,因为我不知道如何在编译时以一种方式改变字符串字面量,使其成为窄字符或宽字符。

考虑以下基于TCHAR的函数:

// quote the given string in-place using the given quote character
inline void MakeQuoted(CString & str, TCHAR chQuote = _T('"'))
{
    if (str.IsEmpty() || str[0] != chQuote)
        str.Format(_T("%c%s%c"), chQuote, str, chQuote);
}

我希望你能将其制作成模板:

我想将其制作成模板:

// quote the given string in-place using the given quote character
template <typename CSTRING_T, typename CHAR_T>
inline void MakeQuoted(CSTRING_T & str, CHAR_T chQuote = '"')
{
    if (str.IsEmpty() || str[0] != chQuote)
        str.Format("%c%s%c", chQuote, str, chQuote);
}

立即,我们就遇到了两个字符串字面量的问题('"' 和 "%c%s%c")。

如果对于 CSTRING_T = CStringA,CHAR_T = char,那么上述字面量是正确的。但是如果它被用于 CStringW 和 wchar_t,则我需要 (L'"' 和 L"%c%c%c")。

因此,我需要找到一种方法来做如下操作:

template <typename CSTRING_T, typename CHAR_T>
inline void MakeQuoted(CSTRING_T & str, CHAR_T chQuote = Literal<CHAR_T>('"'))
{
    if (str.IsEmpty() || str[0] != chQuote)
        str.Format(Literal<CHAR_T>("%c%s%c"), chQuote, str, chQuote);
}

我遇到了困难:我该怎么做才能让Literal(字符串或字符字面值)实际上会生成L"string"或"string",具体取决于CHAR_T?

编辑:有100多个函数,其中许多更复杂,其中有更多的字符串文字需要同时适用于窄字符串和宽字符串。除了复制每个这样的函数,然后编辑每个函数以使其成为宽字符串或窄字符串之外,肯定有一种技术可以允许一个定义根据CHAR_T变化而不同吧?


我将回答Mark Ransom提供的混合宏+模板的答案,但我想包括一个更完整的解决方案(对于任何在意的人),因此在这里:

// we supply a few helper constructs to make templates easier to write
// this is sort of the dark underbelly of template writing
// to help make the c++ compiler slightly less obnoxious

// generates the narrow or wide character literal depending on T
// usage: LITERAL(charT, "literal text") or LITERAL(charT, 'c')
#define LITERAL(T,x) template_details::literal_traits<typename T>::choose(x, L##x)

namespace template_details {

    // Literal Traits uses template specialization to achieve templated narrow or wide character literals for templates
    // the idea came from me (Steven S. Wolf), and the implementation from Mark Ransom on stackoverflow (https://dev59.com/ulLTa4cB1Zd3GeqPXiA1)
    template<typename T>
    struct literal_traits
    {
        typedef char char_type;
        static const char * choose(const char * narrow, const wchar_t * wide) { return narrow; }
        static char choose(const char narrow, const wchar_t wide) { return narrow; }
    };

    template<>
    struct literal_traits<wchar_t>
    {
        typedef wchar_t char_type;
        static const wchar_t * choose(const char * narrow, const wchar_t * wide) { return wide; }
        static wchar_t choose(const char narrow, const wchar_t wide) { return wide; }
    };

} // template_details

此外,我创建了一些辅助工具,使得编写利用此概念与CStringT<>结合的模板更加容易、易读和易懂:

// generates the correct CString type based on char_T
template <typename charT>
struct cstring_type
{
    //  typedef CStringT< charT, ATL::StrTraitATL< charT, ATL::ChTraitsCRT< charT > > > type;
    // generate a compile time error if we're invoked on a charT that doesn't make sense
};

template <>
struct cstring_type<char>
{
    typedef CStringA type;
};

template <>
struct cstring_type<wchar_t>
{
    typedef CStringW type;
};

#define CSTRINGTYPE(T) typename cstring_type<T>::type

// returns an instance of a CStringA or CStringW based on the given char_T
template <typename charT>
inline CSTRINGTYPE(charT) make_cstring(const charT * psz)
{
    return psz;
}

// generates the character type of a given CStringT<>
#define CSTRINGCHAR(T) typename T::XCHAR

通过上述方法,可以编写模板以根据CStringT<>或char/wchar_t参数生成正确的CString变体。例如:

// quote the given string in-place using the given quote character
template <typename cstringT>
inline void MakeQuoted(cstringT & str, CSTRINGCHAR(cstringT) chQuote = LITERAL(CSTRINGCHAR(cstringT), '"'))
{
    if (str.IsEmpty() || str[0] != chQuote)
        str.Format(LITERAL(cstringT::XCHAR, "%c%s%c"), chQuote, str, chQuote);
}

// return a quoted version of the given string
template <typename cstringT>
inline cstringT GetQuoted(cstringT str, CSTRINGCHAR(cstringT) chQuote = LITERAL(CSTRINGCHAR(cstringT), '"'))
{
    MakeQuoted(str, chQuote);
    return str;
}

我知道我可以使用函数重载来生成两个MakeQuoted的定义,而不是使用单个模板,这样我就不需要担心字面量(请参见@In silico的答案)。 但是,考虑到我只是为了提供两组不同的字面量而逐字重复所有代码,这似乎很愚蠢。 肯定有一些方法可以使用元编程来动态生成正确的字面量(类型相关的字面量)吧? - Mordachai
@Mordachai:不使用元编程。如果使用了,我们也可以切换字符串。您能否举例说明一些语法,以便使用此函数? - John Dibling
我不确定这是否是正确的情况。模板会根据提供给模板的类型参数生成代码。我在这里寻找的是一种随所提供的类型而变化的文字。也许是模板特化?!就像我希望编译器对字符串字面量进行自动类型提升(考虑标量参数,模板将无需努力即可工作,因为标量字面量会自动提升到适当的类型,但对于字符串字面量则不然)。:( - Mordachai
1
慢速配对编程 - 我喜欢它。 - Mark Ransom
@Mark - 不幸的是,我已经独自为雇主工作了一年,而在过去的11年中,我没有遇到过其他甚至和我同样水平(更不用说顶尖人才)的程序员。因此,Stack Overflow是一个很好的资源,可以回答我无法深入解答的问题。;) - Mordachai
显示剩余2条评论
7个回答

5

这个概念是使用宏来生成两种文字形式,即charwchar_t,然后让模板函数选择适合上下文的一种。

请记住,模板函数在没有其他调用它们的代码时不会生成任何代码。大多数情况下这并不重要,但对于库来说就很重要。

这段代码未经测试,但我相信它可以工作。

#define LITERAL(T,x) CString_traits<T>::choose(x, L##x)

template<typename T>
struct CString_traits
{
    typedef char char_type;
    static const char * choose(const char * narrow, const wchar_t * wide) { return narrow; }
    static char choose(char narrow, wchar_t wide) { return narrow; }
};

template<>
struct CString_traits<CStringW>
{
    typedef wchar_t char_type;
    static const wchar_t * choose(const char * narrow, const wchar_t * wide) { return wide; }
    static wchar_t choose(char narrow, wchar_t wide) { return wide; }
};

template <typename T>
inline void MakeQuoted(T & str, CString_traits<T>::char_type chQuote = LITERAL(T,'"'))
{
    if (str.IsEmpty() || str[0] != chQuote)
        str.Format(LITERAL(T,"%c%s%c"), chQuote, str, chQuote);
}

我能看到的主要问题是,如果调用者想要替换chQuote,他们必须接触到LITCHAR宏,从那个角度来看,这并不是非常干净的。 - Puppy
@DeadMG,我看不到任何避免暴露调用代码给宏的方法。必须同时创建字符和宽字符字面值而不重复自己。 - Mark Ransom
2
我并不是一个语言纯粹主义者。我想要高度可用的代码,这看起来很有前途。我会尝试一下,看看能否让它工作。在成千上万个宏的世界中(Win32编程),多使用一个宏几乎没有任何不利影响。 - Mordachai

1

这段代码是我个人的小小天才之作。

#include <malloc.h>
template<typename to, int size> to* make_stack_temporary(const char(&lit)[size], to* memory = (to*)_alloca(sizeof(to)*size)) {
    for(int i = 0; i < size; i++)
        memory[i] = lit[i];
    return memory;
}

当您在默认参数中使用alloca时,它实际上是从调用者的堆栈中分配的,允许您返回数组而不必求助于堆。没有动态分配,也没有内存释放。_alloca是MSVC提供的CRT函数,因此我不能保证其可移植性 - 但如果您正在使用ATL,那可能不是问题。当然,这也意味着指针不能在调用函数之后保留,但对于临时使用(如格式字符串)应该足够了。还有一些与异常处理有关的注意事项,您可能不太可能遇到(请查看MSDN获取详细信息),当然,它仅适用于具有相同二进制表示的字符,据我所知,这是您可以放入窄字符串字面值的每个字符。我知道这只解决了您可能遇到的实际问题的子集,但它比宏或两次指定每个字面量等特定子集的解决方案要好得多。

您还可以使用明显更丑陋但行为更一致的聚合初始化。

template<typename T> some_type some_func() {
    static const T array[] = { 'a', ' ', 's', 't', 'r', 'i', 'n', 'g', ' ', 'l', 'i', 't', 'e', 'r', 'a', 'l', '\0' };
}

在C++0x中,使用可变参数模板可能会使这个解决方案不那么糟糕。我已经接近一个更好的解决方案,它是C++03,但不要抱太大希望。
编辑:你可以这样做,我认为这是最好的解决方案,仍然需要一些折腾。
#include <iostream>
#include <array>
#include <string>

struct something {
    static const char ref[];
};

const char something::ref[] = "";

template<int N, const char(*t_ref)[N], typename to> struct to_literal {
private:
    static to hidden[N];
public:
    to_literal() 
    : ref(hidden) {
        for(int i = 0; i < N; i++)
            hidden[i] = (*t_ref)[i];
    }
    const to(&ref)[N];
};
template<int N, const char(*t_ref)[N], typename to> to to_literal<N, t_ref, to>::hidden[];

template<int N, const char(&ref)[N], typename to> const to* make_literal() {
    return to_literal<N, &ref, to>().ref;
}

int main() {
    std::wcout << make_literal<sizeof(something::ref), something::ref, wchar_t>();
    std::wcin.get();
}

你需要遍历每个文字常量,并将其变成结构体的静态成员,然后引用它,这样会更加有效。


@Mordachai:我也认为这种情况下语言表达不够好,需要大幅度改进。 - Puppy

0
我有一个类似的情况。我编写了1个源代码文件和一个头文件(当然),将其从构建中排除。然后创建了另外2个包含原始源代码的源文件,通过#include指令将它们包含进来。第一个文件中,在包含之前,我使用#define UNICODE(如果未定义)。在另外一个文件中,我使用#undef UNICODE(如果已定义)。源文件包含一些静态结构和若干函数,这些函数的文本内容都是相同的,但参数类型分别为wchar_t或char(编译时不同)。如果每个函数的参数都为wchar_t或char,那么这种方法将产生2组重载函数或2组不同命名的函数(取决于头文件的编写方式,以tchar.h为例)。现在,应用程序可以使用UNICODE和ANSI版本的函数,并且如果头文件被正确编写,则还可以使用TCHAR的默认版本。如果您希望,我可以详细说明,只需告诉我即可。

包装技巧也可以用于头文件,但要注意重新定义或取消定义UNICODE。 - engf-010
我考虑过这个。我想只要设置_T、TEXT和UNICODE,就可以满足我的需求了。但是如果你尝试调用_ttoi或_tcscpy等函数,那么这些函数之前不是已经定义为_mbs或str了吗?CRT头文件使用#pragma once指令,对吧? - Mordachai
我自己没有使用CRT例程,而是使用了WINDOWS API(以避免多线程问题)。但是,如果它有效的话,你应该尝试一下,这可以节省很多工作和调试时间。但请确保不要使用预编译头文件,因为那会阻止这种技术的工作。只有在包装源代码时才可以使用pp。 - engf-010
我认为 #pragma once 的意思是在编译一个特定源文件时只包含一次头文件,因此我认为你不会遇到麻烦。 - engf-010
我也提交了一个关于这个技术的问题:“一个源有多个对象”。 - engf-010

0

像这样的东西,您不需要使用模板,因为只有两种使用MakeQuoted()的方法。您可以使用函数重载来实现相同的目的:

inline void MakeQuoted(CStringA& str, char chQuote = '"') 
{ 
    if (str.IsEmpty() || str[0] != chQuote) 
        str.Format("%c%s%c", chQuote, str, chQuote); 
} 


inline void MakeQuoted(CStringW& str, wchar_t chQuote = L'"') 
{ 
    if (str.IsEmpty() || str[0] != chQuote) 
        str.Format(L"%c%s%c", chQuote, str, chQuote); 
} 

毫无疑问,这是最简单的方法,而不必使用宏,假设您尝试使用模板为字符串实用程序库提供解决方案。

您可以将长且复杂的函数的常见功能分解:

template<typename CStrT, typename CharT>
inline void MakeQuotedImpl(CStrT& str, CharT chQuote,
    const CharT* literal)
{
    if (str.IsEmpty() || str[0] != chQuote) 
        str.Format(literal, chQuote, str, chQuote); 

}

inline void MakeQuoted(CStringA& str, char chQuote = '"') 
{ 
    MakeQuotedImpl(str, chQuote, "%c%s%c");
} 


inline void MakeQuoted(CStringW& str, wchar_t chQuote = L'"') 
{
    MakeQuotedImpl(str, chQuote, L"%c%s%c");
} 

这正是我目前正在做的事情。它并不非常可扩展(我们有超过一百个类似上述函数的函数)。 - Mordachai
根据您对John Dibling答案的评论,似乎您想创建一个库,可以在单个项目中使用基于charwchar_t的字符串,而不考虑编译器设置。不幸的是,除了函数重载之外,您没有更多的选择 - 对于这种特殊情况,函数重载是“最简单”的方法。我知道Windows API有两个版本的每个接受字符串的函数(例如CreateWindowA()CreateWindowW())。 - In silico
只要没有字面量,我就可以使用模板。一旦出现字面量,我就必须诉诸于函数重载(有时我可以将所有必要的参数模板化为一个单一模板,然后创建另外两个函数,分别使用char/wchar_t和提供所需参数和适当字面量的底层模板版本)。 - Mordachai
也许有点跑题,但是...如果你想写:stream << '"' << argument << '"',<< 对你有什么帮助呢?现在我正在使用流和插入运算符,但字面值的事实仍然存在(即它们只有在底层流被假定为编码窄字符时才正确)。 - Mordachai
@DeadMG - 再说一遍,微软代码中设定的标准是不容置疑的。每隔一段时间就冒出来发表一些愚蠢的评论并不能解决当前存在的问题,这完全是你个人的观点而且毫无意义。 - Mordachai
显示剩余8条评论

-1
你可以使用模板偏特化来实现MarkQuoted,并根据类型进行引用。

-1

我相信你想要使用 TEXT MFC 宏:

TCHAR* psz = TEXT("Hello, generic string");

如果我回答了你没有问的问题,请告诉我,我会删除这个回答。 - John Dibling
如果我需要的是随编译器设置而变化的字符串,而不是正在处理的字符串类型,那么这个方法是可行的。这对于客户端代码非常有效(应用程序源代码),但对于库代码来说并不是很有帮助,因为在单个项目中无论编译器的UNICODE或MBCS构建目标如何,都会使用窄字符串和宽字符串。 - Mordachai

-1

好的,如果你真的想要将这个模板化,我认为我能想到的最好的方法是创建一个基于this discussion的存储文字的模板类。大概像这样:

template <typename T> class Literal;
template <> class Literal<char>
{
public:
    static const char Quote = '"';
};
template <> class Literal<wchar_t>
{
public:
    static const wchar_t Quote = L'"';
};

然后,在您的非专用但模板化函数中使用Literal<CHAR_T>::Quote。有点凌乱,我猜,但它的好处是不会重复您的函数逻辑,并为您提供了模板化字符串字面量。


与上述答案相同:这取决于编译器构建目标,而不是传入的字符串类型。我有一些项目需要在同一构建中使用窄字符和宽字符字符串(遗留文件为窄字符,Win32 API为宽字符等)。 - Mordachai
谢谢 - 我考虑将其作为一个策略类来实现。 只要我愿意把各种任意字符串放进去供使用(我可以将其放在一个专门用于该头文件的详细信息命名空间中...),它就能起作用。 - Mordachai

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接