如何避免C++函数意外陷入递归?以字符串重载函数为例
现有get_val_from_db函数提供了针对signed char和unsigned char的重载,同时有一个模板重载用于将std::string类型的key转换为const char*。但当传入char类型的参数时,由于无法匹配到前两个非模板重载,编译器会选择模板版本。而模板内部调用get_val_from_db(key.c_str(), value)时,const char*会被隐式转换为std::string,导致模板递归调用,最终引发栈溢出。
需要在保留std::string转const char*功能的前提下,让不支持的类型触发编译错误而非递归,使用C++17编译器。
原代码如下:
#include <iostream> void get_val_from_db(const char* key, signed char& value) { std::cout << "Getting value from db with signed char!\n"; } void get_val_from_db(const char* key, unsigned char& value) { std::cout << "Getting value from db with unsigned char!\n"; } // overload to convert first argument from std::string to const char* template <typename T> void get_val_from_db(const std::string& key, T& value) { get_val_from_db(key.c_str(), value); } int main() { unsigned char my_value_unsigned = 0; // OK! get_val_from_db("my key", my_value_unsigned); signed char my_value_signed = 0; // OK! get_val_from_db("my key", my_value_signed); char my_value_char = 0; // NOK! Can't convert! Becomes recursive! get_val_from_db("my key", my_value_char); // Can't remove the std::string conversion altogether because these needs to keep working std::string key = "my key"; get_val_from_db(key, my_value_signed); get_val_from_db(key, my_value_unsigned); }
我们可以通过C++17的约束机制或SFINAE特性,确保模板重载仅在存在对应的const char*版本重载时才生效,避免递归调用并触发编译错误。
方法1:使用requires子句(C++17及以上)
通过requires子句显式要求存在get_val_from_db(const char*, T&)的有效重载,这样当T是不支持的类型(如char)时,该模板不会参与重载解析,编译器会直接报错找不到匹配的函数:
#include <iostream> #include <string> void get_val_from_db(const char* key, signed char& value) { std::cout << "Getting value from db with signed char!\n"; } void get_val_from_db(const char* key, unsigned char& value) { std::cout << "Getting value from db with unsigned char!\n"; } // 仅当存在对应的const char*版本重载时,该模板才生效 template <typename T> requires requires(const char* k, T& v) { get_val_from_db(k, v); } void get_val_from_db(const std::string& key, T& value) { get_val_from_db(key.c_str(), value); } int main() { unsigned char my_value_unsigned = 0; get_val_from_db("my key", my_value_unsigned); // OK signed char my_value_signed = 0; get_val_from_db("my key", my_value_signed); // OK char my_value_char = 0; // get_val_from_db("my key", my_value_char); // 编译错误:无匹配函数 std::string key = "my key"; get_val_from_db(key, my_value_signed); // OK get_val_from_db(key, my_value_unsigned); // OK }
方法2:使用SFINAE与decltype
通过返回类型的decltype检测get_val_from_db(const char*, T&)是否可行,同样可以排除不支持的类型:
#include <iostream> #include <string> void get_val_from_db(const char* key, signed char& value) { std::cout << "Getting value from db with signed char!\n"; } void get_val_from_db(const char* key, unsigned char& value) { std::cout << "Getting value from db with unsigned char!\n"; } // SFINAE:仅当get_val_from_db(const char*, T&)有效时,模板才被实例化 template <typename T> auto get_val_from_db(const std::string& key, T& value) -> decltype(get_val_from_db(std::declval<const char*>(), value)) { get_val_from_db(key.c_str(), value); } int main() { // 同方法1的测试代码 }
方法3:使用static_assert显式检查类型
如果希望直接明确支持的类型,可以在模板内使用static_assert,直接拒绝非支持类型的实例化:
#include <iostream> #include <string> #include <type_traits> void get_val_from_db(const char* key, signed char& value) { std::cout << "Getting value from db with signed char!\n"; } void get_val_from_db(const char* key, unsigned char& value) { std::cout << "Getting value from db with unsigned char!\n"; } template <typename T> void get_val_from_db(const std::string& key, T& value) { // 仅允许signed char或unsigned char类型 static_assert(std::is_same_v<T, signed char> || std::is_same_v<T, unsigned char>, "get_val_from_db only supports signed char or unsigned char as value type"); get_val_from_db(key.c_str(), value); } int main() { // 同方法1的测试代码 }
这种方式的缺点是后续添加新的支持类型时,需要手动更新static_assert的条件,而前两种方法会自动适配新的const char*重载。
内容的提问来源于stack exchange,提问作者Henrique Jung

