如何防止函数意外递归?

Hen*_*ung 6 c++ string templates c++17

考虑以下 C++ 代码:

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

}
Run Code Online (Sandbox Code Playgroud)

我有一个get_val_from_db接收键名称和键类型的函数,每种类型都有许多专门化,包括一个包罗万象的模板版本,它仅将键名称从 转换为std::stringconst char*因为这些不能自动完成。

在编译器资源管理器上检查此代码的实时示例char,请注意,当调用作为类型传递的函数时,程序崩溃了。发生这种情况char是因为不可转换为signed char&请参阅另一个编译器资源管理器示例),因此在传递时char,要调用的最佳可用函数是void get_val_from_db(const std::string& key, T& value)然后调用自身的函数,因为const char* 可以直接转换为std::string。因此调用变成递归并导致堆栈溢出。

我当然可以添加一个新的专业化char,但我想让代码对未来的开发人员来说更安全。我希望新的参数类型不被捕获void get_val_from_db(const std::string& key, T& value),而是得到一个漂亮而有趣的编译错误。我不想完全删除该函数,因为我们需要进行std::string转换const char*以保持当前代码正常工作。综上所述,如何防止这种从const char*到 的隐式转换的std::string发生?

template <typename T>
void get_val_from_db(const std::string& key, T& value)
{
    // how to prevent key.c_str() from being converted to std::string?
    get_val_from_db(key.c_str(), value);
}
Run Code Online (Sandbox Code Playgroud)

我有一个 C++ 17 编译器可供使用。

Jan*_*tke 6

一般来说,有两种方法可以确保重载集中的函数不被选中:

  1. 声明在重载决策中获胜的其他函数
  2. 限制重载,使其无法通过某些参数进行选择
  3. 拆分过载集

您已经认识到可以通过添加以下内容来执行 1. 操作:

void get_val_from_db(const char* key, char& value)
{
    std::cout << "Getting value from db with signed char!\n";
}
Run Code Online (Sandbox Code Playgroud)

这是一种有效的解决方案,但还有其他解决方案:

删除的功能

void get_val_from_db(const std::string& key, char&) = delete;
Run Code Online (Sandbox Code Playgroud)

如果调用此函数,您将收到编译器错误。与模板相比,它在重载解析方面会获胜,因为它是非模板。

约束条件

C++17 允许您使用 SFINAE 约束函数。std::enable_if是一个常用工具:

template <typename T>
auto get_val_from_db(const std::string& key, T& value)
  -> std::enable_if_t<!std::is_same_v<char, T>>
{
    get_val_from_db(key.c_str(), value);
}
Run Code Online (Sandbox Code Playgroud)

通过 clang,我们可以得到非常好的诊断std::enable_if

<source>:31:5: error: no matching function for call to 'get_val_from_db'
    get_val_from_db("my key", my_value_char);
    ^~~~~~~~~~~~~~~
...
<source>:16:6: note: candidate template ignored: requirement '!std::is_same_v<char, char>' was not satisfied [with T = char]
auto get_val_from_db(const std::string& key, T& value)
     ^
Run Code Online (Sandbox Code Playgroud)

拆分重载集

您还可以通过将“高级”功能与某些“详细”功能分开来解决此问题。我们可以让用户总是调用函数 template get_val_from_db,并且它将分派给较低级别​​的函数:

// note: prefer std::string_view over const char* or const std::string&
//       (it can be converted from both)
void get_schar_from_db(std::string_view key, signed char& value)
{
    std::cout << "Getting value from db with signed char!\n";
}

void get_uchar_from_db(std::string_view key, unsigned char& value)
{
    std::cout << "Getting value from db with unsigned char!\n";
}

template <typename T>
void get_val_from_db(std::string_view key, T& value)
{
    if constexpr (std::is_same_v<T, signed char>) {
        return get_schar_from_db(key, value);
    }
    else if constexpr (std::is_same_v<T, unsigned char>) {
        return get_uchar_from_db(key, value);
    }
    else {
        // TODO: provide fallback case, dispatch to more special cases, etc.
    }
}
Run Code Online (Sandbox Code Playgroud)

您还可以将其与上面的约束方法混合使用,并且只需将约束施加到向用户公开的顶级函数上。


Ted*_*gmo 2

我想要的是,如果我提供一个不专门用于其自身功能的参数,则会出现编译错误

然后,您可以在不存在重载的所有情况下, SFINAE取消函数模板的实例化:void get_val_from_db(const char*, T&)

// convert first argument from std::string to const char*
// only if  "void get_val_from_db(const char*, T&)"  exists
template <class T>
auto get_val_from_db(const std::string& key, T& value) -> 
    std::void_t<decltype(static_cast<void(*)(const char*, T&)>(get_val_from_db))> 
{
    get_val_from_db(key.c_str(), value);
}
Run Code Online (Sandbox Code Playgroud)

演示


为了获得更清晰的错误消息,您可以创建一个类型特征来检查是否void get_val_from_db(const char*, T&)存在 的实现并使用 astatic_assert代替。

#include <utility>

// helper type trait
template <class T>
struct has_implementation {
    static std::false_type test(...);

    template <class U>
    static auto test(U)
        -> decltype(static_cast<void(*)(const char*, U&)>(get_val_from_db),
                    std::true_type{});

    static constexpr bool value = decltype(test(std::declval<T>()))::value;
};

template <class T>
inline constexpr bool has_implementation_v = has_implementation<T>::value;
Run Code Online (Sandbox Code Playgroud)
template <class T>
void get_val_from_db(const std::string& key, T& value) {
    // very clear error message:
    static_assert(has_implementation_v<T>, "Impl. for T missing");

    get_val_from_db(key.c_str(), value);
}
Run Code Online (Sandbox Code Playgroud)

演示