C++如何完美转发并特化函数首参数?合并日志函数遇编译错误
我尝试将LoggerBase类的两个模板函数LogMessage与LogMessageW合并为一个通用的LogMessageAny函数:
原函数实现:
template<class ...T> auto LoggerBase::LogMessage(T&&... args) -> void { if (Verbose) { constexpr int size{ 256 }; char msgBuffer[size]; auto size_copied = sprintf_s(msgBuffer, size, std::forward<T>(args)...); std::string msg(msgBuffer); LogMessageQuery.Add(msg); } } template<class ...T> auto LoggerBase::LogMessageW(T&&... args) -> void { if (Verbose) { constexpr int size{ 256 }; wchar_t msgBuffer[size]; auto size_copied = swprintf_s(msgBuffer, size, std::forward<T>(args)...); std::wstring wmsg(msgBuffer); LogMessageQuery.Add(wmsg); } }
合并后的函数实现:
template<class ...T> auto LoggerBase::LogMessageAny(T&&... args) -> void { if (Verbose) { using firstType = typename std::tuple_element<0, std::tuple<T...>>::type; constexpr int size{ 256 }; if (typeid(firstType) == typeid(const wchar_t*)) { wchar_t msgBuffer[size]; auto size_copied = swprintf_s(msgBuffer, size, std::forward<T>(args)...); std::wstring wmsg(msgBuffer); LogMessageQuery.Add(wmsg); } else if (typeid(firstType) == typeid(const char*)) { char msgBuffer[size]; auto size_copied = sprintf_s(msgBuffer, size, std::forward<T>(args)...); std::string msg(msgBuffer); LogMessageQuery.Add(msg); } } }
测试代码无法编译:
typedef void(__stdcall* MessageChangedCallback)(const wchar_t* string); static MessageChangedCallback log = [](const wchar_t* z) { Logger::WriteMessage(z); }; TEST_CLASS(LoggerBaseTests) { public: TEST_METHOD(LogMessageAny_stringTest) { LoggerBase sut(log); sut.LogMessageAny("This is a good std::string TEST %i!", 123); } TEST_METHOD(LogMessageAny_wstringTest) { LoggerBase sut(log); sut.LogMessageAny(L"This is a good std::wstring TEST %i!", 456); } };
编译错误:
Error C2664 'int swprintf_s(wchar_t *const ,const size_t,const wchar_t *const ,...)': cannot convert argument 3 from 'const char [36]' to 'const wchar_t *const '
Error C2664 'int sprintf_s(char *const ,const size_t,const char *const ,...)': cannot convert argument 3 from 'const wchar_t [37]' to 'const char *const '
原两个函数可正常工作,现咨询:
- 当前类型判断方式是否合理?
- 如何特化函数首参数并完美转发剩余参数?
- 是否应在函数参数层面进行特化或重载?
1. 当前类型判断方式是否合理?
不合理。typeid是运行时类型检查,但模板函数的参数类型匹配是编译时完成的。你写的if-else分支在编译时会被全部实例化,不管firstType实际是什么类型。比如调用LogMessageAny("abc", 123)时,编译器会同时尝试编译swprintf_s和sprintf_s的分支,而swprintf_s需要宽字符格式串,传入const char*必然导致编译错误,这就是你现在遇到的问题。
另外,firstType的推导也有问题:当传入字符串字面量"abc"时,T的类型是const char(&)[N](引用类型),typeid(firstType)得到的是数组类型,和typeid(const char*)不匹配;同理L"abc"的firstType是const wchar_t(&)[N],和typeid(const wchar_t*)也不匹配,这会导致你的分支判断在运行时根本不会进入正确的逻辑。
2. 如何特化函数首参数并完美转发剩余参数?
可以通过编译时类型判断(比如std::is_same_v结合constexpr if),或者拆分模板参数,把首参数单独提取出来,利用模板重载来区分字符类型。
方式一:使用constexpr if(C++17及以上)
修改LogMessageAny,直接根据首参数的字符类型在编译时选择分支,避免无效分支的实例化:
#include <type_traits> template<class CharT, class... Args> auto LoggerBase::LogMessageAny(CharT* fmt, Args&&... args) -> void { if (Verbose) { constexpr int size{256}; if constexpr (std::is_same_v<CharT, char>) { char msgBuffer[size]; auto size_copied = sprintf_s(msgBuffer, size, fmt, std::forward<Args>(args)...); std::string msg(msgBuffer); LogMessageQuery.Add(msg); } else if constexpr (std::is_same_v<CharT, wchar_t>) { wchar_t msgBuffer[size]; auto size_copied = swprintf_s(msgBuffer, size, fmt, std::forward<Args>(args)...); std::wstring wmsg(msgBuffer); LogMessageQuery.Add(wmsg); } } }
这里把首参数单独提取为CharT*,字符串字面量会隐式转换为对应的指针类型,constexpr if会在编译时只保留符合条件的分支,不会触发另一个分支的编译错误。
方式二:模板重载(兼容C++11/14)
针对char*和wchar_t*分别重载模板函数,完美转发剩余参数:
// 处理窄字符格式串 template<class... Args> auto LoggerBase::LogMessageAny(const char* fmt, Args&&... args) -> void { if (Verbose) { constexpr int size{256}; char msgBuffer[size]; auto size_copied = sprintf_s(msgBuffer, size, fmt, std::forward<Args>(args)...); std::string msg(msgBuffer); LogMessageQuery.Add(msg); } } // 处理宽字符格式串 template<class... Args> auto LoggerBase::LogMessageAny(const wchar_t* fmt, Args&&... args) -> void { if (Verbose) { constexpr int size{256}; wchar_t msgBuffer[size]; auto size_copied = swprintf_s(msgBuffer, size, fmt, std::forward<Args>(args)...); std::wstring wmsg(msgBuffer); LogMessageQuery.Add(wmsg); } }
这种方式更直接,编译器会根据传入的格式串类型自动匹配对应的重载版本,完全避免了类型判断的问题,兼容性也更好。
3. 是否应在函数参数层面进行特化或重载?
优先选择重载,而不是模板特化。
模板特化的规则比较复杂,尤其是针对可变参数模板的特化,容易出现匹配优先级问题;而重载的逻辑更直观,编译器的匹配规则更清晰,代码可读性也更强。
另外,如果你想保留单个函数名的同时处理两种字符类型,上面提到的constexpr if方式是比较简洁的选择(C++17+);如果需要兼容旧标准,重载两个模板函数是更稳妥的方案,效果和你原来的LogMessage/LogMessageW一致,但合并成了同一个函数名,调用更方便。
内容的提问来源于stack exchange,提问作者Soleil

