如何防止函数意外成为递归函数?

huangapple go评论99阅读模式
英文:

How to prevent a function from accidentally becoming recursive?

问题

我有一个名为get_val_from_db的函数,它接收一个键名和一个键类型,对于每种类型都有许多专门化版本,包括一个通用的模板版本,仅将键名从std::string转换为const 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::stringconst char*的转换来保持当前的代码正常工作。总之,如何防止从const char*std::string的这种隐式转换发生?

template <typename T>
void get_val_from_db(const std::string& key, T& value)
{
    // 如何防止 key.c_str() 转换为 std::string?
    get_val_from_db(key.c_str(), value);
}

我拥有一个支持C++ 17的编译器。

英文:

Consider the following C++ code:

#include &lt;iostream&gt;

void get_val_from_db(const char* key, signed char&amp; value)
{
    std::cout &lt;&lt; &quot;Getting value from db with signed char!\n&quot;;
}

void get_val_from_db(const char* key, unsigned char&amp; value)
{
    std::cout &lt;&lt; &quot;Getting value from db with unsigned char!\n&quot;;
}

// overload to convert first argument from std::string to const char*
template &lt;typename T&gt;
void get_val_from_db(const std::string&amp; key, T&amp; value)
{
    get_val_from_db(key.c_str(), value);
}


int main()
{
    unsigned char my_value_unsigned = 0; // OK!
    get_val_from_db(&quot;my key&quot;, my_value_unsigned);

    signed char my_value_signed = 0; // OK!
    get_val_from_db(&quot;my key&quot;, my_value_signed);
    
    char my_value_char = 0; // NOK! Can&#39;t convert! Becomes recursive!
    get_val_from_db(&quot;my key&quot;, my_value_char);

    // Can&#39;t remove the std::string conversion altogether because these needs to keep working
    std::string key = &quot;my key&quot;;
    get_val_from_db(key, my_value_signed);
    get_val_from_db(key, my_value_unsigned);

}

I have a function get_val_from_db that receives a key name and a key type, with many specializations for each type, including a catch-all template version that merely converts keys names from std::string to const char*, since those can't be done automatically.

Check a live example of this code on compiler explorer, notice that the program crashes when calling the function passing char as a type. This happens because char is not convertible to signed char&amp; (see another compiler explorer example), so when passing char, the best available function to be called is void get_val_from_db(const std::string&amp; key, T&amp; value) which then calls itself, since const char* can be converted to std::string directly. So the call becomes recursive and causes a stack overflow.

I could of course add a new specialization for char, but I want to make the code safer for future developers. I want new parameter types not to be captured by void get_val_from_db(const std::string&amp; key, T&amp; value) and instead get a nice and juicy compilation error. I don't want to remove the function altogether because we need the std::string to const char* conversion to keep the current code working. In summary, how to prevent this implicit conversion from const char* to std::string from happenning?

template &lt;typename T&gt;
void get_val_from_db(const std::string&amp; key, T&amp; value)
{
    // how to prevent key.c_str() from being converted to std::string?
    get_val_from_db(key.c_str(), value);
}

I have a C++ 17 compiler at my disposal.

答案1

得分: 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";
}

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

已删除的函数

void get_val_from_db(const std::string& key, char&) = delete;

如果调用了这个函数,将会得到编译器错误。与模板不同,它在重载解析中获胜,因为它是一个非模板。

约束

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

使用 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)
     ^

分割重载集

你还可以通过将“高级”函数与一些“详细”函数分离来解决这个问题。
我们可以使用户始终调用函数模板 get_val_from_db,而它将分派给低级函数:

// 注意:优先使用 std::string_view 而不是 const char* 或 const std::string&
//       (它可以从两者中都转换)
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: 提供备用情况,分派到更特殊的情况等。
    }
}

你还可以将这个方法与上面的 约束 方法相结合,只需将约束放在向用户公开的顶级函数上即可。

英文:

In general, there are two ways to ensure your function in an overload set doesn't get selected:

  1. Declare other functions which win in overload resolution
  2. Constrain the overload so it cannot be selected with some arguments
  3. Split up the overload set

You've already recognized that you could do 1. by adding:

void get_val_from_db(const char* key, char&amp; value)
{
    std::cout &lt;&lt; &quot;Getting value from db with signed char!\n&quot;;
}

It's one valid solution, but there are others:

Deleted Function

void get_val_from_db(const std::string&amp; key, char&amp;) = delete;

If this function ever gets called, you will get a compiler error. It will win in overload resolution compared to the template, because it is a non-template.

Constraints

C++17 allows you to constrain functions with SFINAE. std::enable_if is a common tool for that:

template &lt;typename T&gt;
auto get_val_from_db(const std::string&amp; key, T&amp; value)
  -&gt; std::enable_if_t&lt;!std::is_same_v&lt;char, T&gt;&gt;
{
    get_val_from_db(key.c_str(), value);
}

With clang, we get really good diagnostics for std::enable_if:

&lt;source&gt;:31:5: error: no matching function for call to &#39;get_val_from_db&#39;
    get_val_from_db(&quot;my key&quot;, my_value_char);
    ^~~~~~~~~~~~~~~
...
&lt;source&gt;:16:6: note: candidate template ignored: requirement &#39;!std::is_same_v&lt;char, char&gt;&#39; was not satisfied [with T = char]
auto get_val_from_db(const std::string&amp; key, T&amp; value)
     ^

Splitting up the Overload Set

You can also solve this issue by separating the "high level" function from some "detail" functions.
We can make it so that the user always calls the function template get_val_from_db, and it will dispatch to the lower-level ones:

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

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

template &lt;typename T&gt;
void get_val_from_db(std::string_view key, T&amp; value)
{
    if constexpr (std::is_same_v&lt;T, signed char&gt;) {
        return get_schar_from_db(key, value);
    }
    else if constexpr (std::is_same_v&lt;T, unsigned char&gt;) {
        return get_uchar_from_db(key, value);
    }
    else {
        // TODO: provide fallback case, dispatch to more special cases, etc.
    }
}

You can also mix this with the Constraints approach above, and you will only have to put a constraint onto the top-level function that is exposed to the user.

答案2

得分: 4

请注意,以下是已翻译的部分:

> 将前两个重载更名。用调用已更名重载的两个重载替换它们。让第三个重载使用已更名的重载。问题解决。 -
Sam Varshavchik

你需要在低级和高级例程之间进行一些概念上的分离。

// 低级例程
void get_val_from_db_lowlevel(const char* key, signed char& value)
{
    std::cout << "从数据库获取有符号字符的值!\n";
}

void get_val_from_db_lowlevel(const char* key, unsigned char& value)
{
    std::cout << "从数据库获取无符号字符的值!\n";
}

// 高级例程
void get_val_from_db(const char* key, signed char& value)
{
    return get_val_from_db_lowlevel(key, value);
}

void get_val_from_db(const char* key, unsigned char& value)
{
    return get_val_from_db_lowlevel(key, value);
}

// 重载以将第一个参数从std::string转换为const char*
template <typename T>
void get_val_from_db(const std::string& key, T& value)
{
    get_val_from_db_lowlevel(key.c_str(), value);
}

你还可以将低级例程放入命名空间,或将所有例程放入类中,并将低级例程设为 private

英文:

> Rename the first two overloads. Replace them with two overloads that call the renamed overloads. Have the third overload use the renamed overloads. Problem solved. –
Sam Varshavchik

You need some conceptual separation between low-level and higher-level routines.

// Low-level routines
void get_val_from_db_lowlevel(const char* key, signed char&amp; value)
{
    std::cout &lt;&lt; &quot;Getting value from db with signed char!\n&quot;;
}

void get_val_from_db_lowlevel(const char* key, unsigned char&amp; value)
{
    std::cout &lt;&lt; &quot;Getting value from db with unsigned char!\n&quot;;
}

// Higher-level routines
void get_val_from_db(const char* key, signed char&amp; value)
{
    return get_val_from_db_lowlevel(key, value);
}

void get_val_from_db(const char* key, unsigned char&amp; value)
{
    return get_val_from_db_lowlevel(key, value);
}

// overload to convert first argument from std::string to const char*
template &lt;typename T&gt;
void get_val_from_db(const std::string&amp; key, T&amp; value)
{
    get_val_from_db_lowlevel(key.c_str(), value);
}

You could also stuff the low-level routines into a namespace, or stuff all routines into a class and make the low-level ones private.

答案3

得分: 2

以下是您要翻译的内容:

what I want is to get a compilation error if I provide an argument that is not specialized in its own function

您想要的是,如果我提供了一个在其自己的函数中没有专门化的参数,就会得到编译错误。

You could then SFINAE away the instantiation of the function template in all cases where there exists no overload for void get_val_from_db(const char*, T&amp;):

然后,您可以在所有情况下SFINAE掉函数模板的实例化,其中没有重载void get_val_from_db(const char*, T&amp;)

// convert first argument from std::string to const char*
// only if "void get_val_from_db(const char*, T&amp;)" 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);
}

演示

To get a much clearer error message, you could create a type trait to check if an implementation for void get_val_from_db(const char*, T&amp;) exists and use a static_assert instead.

为了获得更清晰的错误消息,您可以创建一个类型特性来检查是否存在void get_val_from_db(const char*, T&amp;)的实现,并改用static_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;
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);
}

演示

英文:

> what I want is to get a compilation error if I provide an argument that is not specialized in its own function

You could then SFINAE away the instantiation of the function template in all cases where there exists no overload for void get_val_from_db(const char*, T&amp;):

// convert first argument from std::string to const char*
// only if  &quot;void get_val_from_db(const char*, T&amp;)&quot;  exists
template &lt;class T&gt;
auto get_val_from_db(const std::string&amp; key, T&amp; value) -&gt; 
    std::void_t&lt;decltype(static_cast&lt;void(*)(const char*, T&amp;)&gt;(get_val_from_db))&gt; 
{
    get_val_from_db(key.c_str(), value);
}

Demo


To get a much clearer error message, you could create a type trait to check if an implementation for void get_val_from_db(const char*, T&amp;) exists and use a static_assert instead.

#include &lt;utility&gt;

// helper type trait
template &lt;class T&gt;
struct has_implementation {
    static std::false_type test(...);

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

    static constexpr bool value = decltype(test(std::declval&lt;T&gt;()))::value;
};

template &lt;class T&gt;
inline constexpr bool has_implementation_v = has_implementation&lt;T&gt;::value;
template &lt;class T&gt;
void get_val_from_db(const std::string&amp; key, T&amp; value) {
    // very clear error message:
    static_assert(has_implementation_v&lt;T&gt;, &quot;Impl. for T missing&quot;);

    get_val_from_db(key.c_str(), value);
}

Demo

huangapple
  • 本文由 发表于 2023年7月3日 22:24:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/76605667.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定