c++templatesvariadic-templatessfinaeconstructor-overloading

Generic constructor template called instead of copy/move constructor


I've designed a simpler wrapper class that adds a label to an object, with the intent of being implicitly convertible/able to replace the wrapped object.

#include <string>
#include <type_traits>
#include <utility>

template < typename T, typename Key = std::string >
class myTag{
    T val;
 public:
    Key key;

    template < typename... Args,
               typename = typename std::enable_if< std::is_constructible< T, Args... >::value >::type >
    myTag(Args&&... args) :
        val(std::forward< Args >(args)...) {
            std::cout << "forward ctor" << std::endl;
    }

    myTag(const myTag& other) :
        key(other.key), val(other.val) {
            std::cout << "copy ctor" << std::endl;
    }

    myTag(myTag&& other):
        key(other.key), val(other.val) {
            std::cout << "move ctor" << std::endl;
    }

    operator T&() { return val; }

    operator const T&() const { return val; }
};


int main(int argc, char const *argv[]) {
    myTag< float > foo(5.6);   // forward ctor
    myTag< float > bar(foo);   // forward ctor

    return 0;
}

However, I am having trouble properly declaring & defining the copy/move constructor. I declared a generic constructor overload template that forwards its arguments to the underlying type, as long as such construction is possible. However, due to the implicit conversion operators, it is capturing every instantiation of myTag, effectively shadowing the copy/move constructors.

The point of non-default copy/move semantics was to copy/move the key value vs default-initializing it with constructor template.

How do I make the compiler prefer/give precedence to the explicit copy/move constructors vs the generic overload? Is there any additional SFINAE check alternative to is_constructible<> that avoids implicit conversions?

EDIT: I should add that I'm looking for a C++14 solution.


Solution

  • It isn't shadowing the copy and move constructors. It is just beating them in overload resolution in some cases.

    The copy and move constructors are not templates, so they will win against a template of the same signature. But the forwarding constructor can be a better match in the cases where the deduced signature differs from the copy and move constructors.

    The idiomatic way to handle this is to remove the forwarding constructor from consideration based on the decayed type of the arguments:

    template <typename... Args>
    constexpr myTag(Args&&... args)
        requires
            ((sizeof...(Args) != 1 && std::is_constructible_v<T, Args...>) ||
             (sizeof...(Args) == 1 && std::is_convertible_v<Args..., T> && !std::is_same_v<myTag, std::decay_t<Args>...>))