使用可变参数重载构造函数

Constructor overloading with variadic arguments

首先,我的代码:

#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 & arg 的构造函数),那么我尝试做的一切都会起作用。也就是带std::string的那个是对的,有效

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

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

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

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

您的代码有两个问题:

  1. 当您将模板参数作为 && 传递给函数模板时,它们被解释为“转发引用”,即它们匹配所有内容,无论它是左值还是右值,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& 而不是 std::string&

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

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

But is there some way I can have both templates and have the compiler figure out which one based on whether the first argument is either a std::string or can be turned into one?

您可以通过将 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<Function, std::string> 为真,则模板将被丢弃并且不考虑重载决议。


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

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"} );
//                                                      ^

进行更改后进行编译。

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

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 &.

接受