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.
CodePudding user response:
Your code has two problems:
When you pass template parameters as
&&
into a function template, they are interpreted as "forwarding references", i.e. 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 passstring{"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...); }
In order to make the other constructor call work, you need to take the string as
const std::string&
notstd::string&
in thewithArgs
functionvoid withArgs(const std::string &) { std::cout << "withArgs()...\n"; }
Full working example here: https://godbolt.org/z/oxEjoEeqn
CodePudding user response:
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.
CodePudding user response:
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 &
.