【问题标题】:Constructor overloading with variadic arguments带有可变参数的构造函数重载
【发布时间】:2022-01-17 07:08:54
【问题描述】:

首先,我的代码:

#include <iostream>
#include <functional>
#include <string>
#include <thread>
#include <chrono>

using std::string;
using namespace std::chrono_literals;

class MyClass {
public:
    MyClass() {}

    // More specific constructor.
    template< class Function, class... Args >
    explicit MyClass( const std::string & theName, Function&& f, Args&&... args )
        : name(theName)
    {
        runner(f, args...);
    }

    // Less specific constructor
    template< class Function, class... Args >
    explicit MyClass( Function&& f, Args&&... args ) {
        runner(f, args...);
    }

    void noArgs() { std::cout << "noArgs()...\n"; }
    void withArgs(std::string &) { std::cout << "withArgs()...\n"; }

    template< class Function, class... Args >
    void runner( Function&& f, Args&&... args ) {
        auto myFunct = std::bind(f, args...);

        std::thread myThread(myFunct);
        myThread.detach();
    }

    std::string name;
};

int main(int, char **) {
    MyClass foo;

    foo.runner (&MyClass::noArgs, &foo);
    foo.runner (&MyClass::withArgs, &foo, std::string{"This is a test"} );

    MyClass hasArgs(string{"hasArgs"}, &MyClass::withArgs, foo, std::string{"This is a test"} );

    std::this_thread::sleep_for(200ms);
}

我正在尝试围绕std::thread 构建一个包装器(插入冗长的原因列表)。考虑将MyClass 在我的实际库中命名为ThreadWrapper

我希望能够构建一个 MyClass 作为std::thread 的直接替代品。这意味着能够做到这一点:

MyClass hasArgs(&MyClass::withArgs, foo, std::string{"This is a test"} );

但我也想选择给线程一个名字,像这样:

MyClass hasArgs(string{"hasArgs"}, &MyClass::withArgs, foo, std::string{"This is a test"} );

所以我创建了两个模板构造函数。如果我只想做一个或另一个并且只使用一个模板构造函数,我正在做的很好。

使用编写的代码,如果你编译 (g++),你会得到严重的错误。如果我注释掉更具体的构造函数,我会得到一组不同的讨厌的错误。如果我注释掉不太具体的构造函数(没有const std::string &amp; arg 的构造函数),那么我正在尝试做的一切工作。也就是说,std::string 是正确的,并且有效。

发生的情况是,如果我有两个构造函数,编译器每次都会选择不太具体的一个。我想强迫它使用更具体的。我想我可以在 C++ 17 中使用 Trait 做到这一点,但我从未使用过它们,而且我不知道从哪里开始。

现在,我将只使用更具体的版本(带有名称的版本)并继续前进。但是我想把不太具体的那个放回去,当我不关心线程名称时使用它。

但是有什么方法可以让我同时拥有两个模板并让编译器根据第一个参数是std::string 还是可以转换为一个来确定哪个模板?

没有人应该在这方面花费大量时间,但如果你看到这个并说,“哦,乔只需要……”那么我很乐意提供帮助。否则我只能忍受这不是 100% 的直接替代品,这很好。

【问题讨论】:

    标签: c++ variadic-templates


    【解决方案1】:

    你的代码有两个问题:

    1. 当您将模板参数作为&amp;&amp; 传递到函数模板时,它们被解释为“转发引用”,即它们匹配所有内容,无论它是左值还是右值,无论是否为 const。更重要的是,它们比某些提供的模板专业化更匹配,这是一个常见的陷阱。在您的具体情况下,您将string{"hasArgs"} 作为右值传递,但专用构造函数需要一个 const 左值引用,因此它被丢弃。要解决此问题,您可以按照您的建议,在这种特定情况下使用类型特征来禁用转发构造函数:

      // Less specific constructor
      template< class Function, class... Args, std::enable_if_t<std::is_invocable_v<Function, Args...>, int> = 0>
      explicit MyClass( Function&& f, Args&&... args ) {
          runner(f, args...);
      }
      
    2. 为了使其他构造函数调用工作,您需要在withArgs函数中将字符串作为const std::string&amp;而不是std::string&amp;

      void withArgs(const std::string &) { std::cout << "withArgs()...\n"; }
      

    完整的工作示例:https://godbolt.org/z/oxEjoEeqn

    【讨论】:

    • hm,这里的代码格式有什么问题?
    • ahm 感谢格式化 :)
    • 这是因为它在枚举中;一些缩进用于将段落与代码与枚举项相关联,然后没有留下任何东西来表示它是代码。您可以进一步缩进,但我建议始终使用三个反引号来开始/结束代码块,而不是使用缩进。
    • OP 的代码不是已经通过 const 引用获取字符串了吗?
    • @cigien 你的,对,对不起。不是构造函数,而是 withArgs 函数必须更改。我已经相应地编辑了我的答案。
    【解决方案2】:

    但是有什么方法可以让我同时拥有两个模板并让编译器根据第一个参数是 std::string 还是可以转换为一个来确定哪个模板?

    您可以通过将 SFINAE 用于更通用的构造函数来做到这一点,例如

    template< class Function, class... Args, 
              std::enable_if_t<!std::is_convertible_v<Function, std::string>, bool> = true>
    explicit MyClass( Function&& f, Args&&... args ) {
       runner(f, args...);
    }
    

    如果std::is_convertible_v&lt;Function, std::string&gt; 为真,则模板将被丢弃且不考虑重载解决方案。


    不知道为什么,但我也不得不改变

    MyClass hasArgs2(string{"hasArgs"}, &MyClass::withArgs, foo, std::string{"This is a test"} );
    

    MyClass hasArgs2(string{"hasArgs"}, &MyClass::withArgs, &foo, std::string{"This is a test"} );
    //                                                      ^
    

    在做出改变后让它编译。

    【讨论】:

    • 只有当第一个参数是std::string 时才禁用更通用的构造函数看起来很奇怪。如果要限制它,为什么不检查Function 是否可调用?
    • @G.Sliepen 因为这是 OP 要求的,所以我向他们展示了他们如何做到这一点。
    • 谢谢。哦,我自己应该注意到&amp;foo 的东西。三个很棒的答案,我从每个答案中学到了不同的东西。
    【解决方案3】:

    如果函数可以通过参数调用,我会让构造函数可行:

    C++20 概念

    class MyClass {
    public:
        MyClass() {}
    
        // More specific constructor.
        template< class Function, class... Args >
            requires std::invocable<Function, Args...>
        explicit MyClass( const std::string & theName, Function&& f, Args&&... args )
            : name(theName)
        {
            runner(f, args...);
        }
    
        // Less specific constructor
        template< class Function, class... Args >
            requires std::invocable<Function, Args...>
        explicit MyClass( Function&& f, Args&&... args ) {
            runner(f, args...);
        }
    };
    

    C++17

    class MyClass {
    public:
        MyClass() {}
    
        // More specific constructor.
        template< class Function, class... Args,
                  std::enable_if_t<std::is_invocable_v<Function, Args...>, std::nullptr_t> = nullptr>
        explicit MyClass( const std::string & theName, Function&& f, Args&&... args )
            : name(theName)
        {
            runner(f, args...);
        }
    
        // Less specific constructor
        template< class Function, class... Args,
                  std::enable_if_t<std::is_invocable_v<Function, Args...>, std::nullptr_t> = nullptr>
        explicit MyClass( Function&& f, Args&&... args ) {
            runner(f, args...);
        }
    };
    

    然而

    现在,如果您在示例中输入此代码,它将无法编译,因为您的代码中还有另一个问题:

    您传递了一个右值字符串,但您的 withArgs 采用了一个左值引用,因此它不满足这个概念。您的代码没有这个概念就可以工作,因为您没有将参数转发给runner,因此 runner 不会收到右值引用。这是您需要解决的问题。将参数转发给runner,然后转发给bindwithArgs 以供const &amp; 使用。

    【讨论】:

    • 非常感谢。我在三个很好的答案中标记为最佳答案,因为您同时展示了 C++ 17 和 20,这太棒了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-02-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-14
    • 1970-01-01
    • 2016-07-26
    相关资源
    最近更新 更多