简体   繁体   中英

Constructor overloading with variadic arguments

First, my code:

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

I'm trying to build a wrapper around std::thread for (insert lengthy list of reasons). Consider MyClass here to be named ThreadWrapper in my actual library.

I want to be able to construct a MyClass as a direct replacement for std::thread . This means being able to do this:

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

But I also want to optionally give threads a name, something like this:

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

So I created two template constructors. If I only want to do one or the other and only use a single template constructor, what I'm doing is fine.

With the code as written, if you compile (g++), you get nasty errors. If I comment out the more specific constructor, I get a different set of nasty errors. If I comment out the less specific constructor (the one that doesn't have a const std::string & arg), then everything I'm trying to do works. That is, the one with std::string is the right one, and it works.

What's happening is that if I have both constructors, the compiler picks the less specific one each time. I want to force it to use the more specific one. I think I can do this in C++ 17 with traits, but I've never used them, and I wouldn't know where to begin.

For now, I'm going to just use the more specific version (the one that takes a name) and move on. But I'd like to put the less specific one back in and use it when I don't care about the thread names.

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?

No one should spend significant time on this, but if you look at this and say, "Oh, Joe just has to..." then I'd love help. Otherwise I'll just live with this not being 100% a direct drop-in replacement, and that's fine.

Your code has two problems:

  1. When you pass template parameters as && into a function template, they are interpreted as "forwarding references", ie they match everything regardless whether it is an lvalue or rvalue, const or not. And more importantly, it's a common pitfall that they are a better match than some provided template specialization. In your concrete case, you pass string{"hasArgs"} as rvalue, but the specialized constructor expects a const lvalue ref, so it is discarded. To fix this, you can, as you suggested, use type traits to disable the forwarding constructor in this specific case:

     // 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. In order to make the other constructor call work, you need to take the string as const std::string& not std::string& in the withArgs function

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

Full working example here: 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?

You can do just that by using SFINAE for the more generic constrcutor like

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

If std::is_convertible_v<Function, std::string> is true then the template will be discarded and not considered for overload resolution.


Not sure why, but I also had to change

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

to

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

to get it to compile after making that change.

I would make the constructors viable iff function is invocable with the arguments:

C++20 concepts

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

However

Now if you put in this code in your example it won't compile because there is another problem in your code:

You pass an rvalue string but your withArgs take an lvalue reference so the concept it not satisfied. Your code works without the concept because you don't forward the arguments to runner so runner doesn't receive an rvalue reference. This is something you need to fix. Forward the arguments to runner and then to bind and withArgs to take by const & .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM