How to write a constructor for a template class using universal reference arguments in C++

Viewed 470

I have a C++ template class MyClass with two type parameters:

template <typename D, typename S>
struct MyClass {
private:
    D d_;
    S s_;
};

I want to add a constructor with D and S references as parameters, so it can get D and S inferred:

template <typename D, typename S>
struct MyClass {
    // can bind to lvalues
    constexpr MyClass(const D &d, const S &s): d_(d), s_(s) {}

    // can bind to rvalues
    constexpr MyClass(D &&d, S &&s): d_(std::move(d)), s_(std::move(s)) {}

private:
    D d_;
    S s_;
};

It works well. I can create objects from either lvalues or rvalues:

auto c1 = MyClass(1, std::string("Hello"));    // rvalues

auto idx = 2;
auto msg = std::string("World");
auto c2 = MyClass(idx, msg);    // lvalues

But I'd like just one constructor using universal references rather than two with each handles lvalues and rvalues. Probably like this:

template <typename D, typename S>
struct MyClass {
    constexpr MyClass(D &&d, S &&s): d_(std::forward<D>(d)), s_(std::forward<S>(s)) {}
private:
    D d_;
    S s_;
};

It does not work for statement "auto w2 = MyClass(i, j);". The compiler complains

error: no viable constructor or deduction guide for deduction of template arguments of 'MyClass'
auto c2 = MyClass(idx, msg);    // lvalues
          ^
note: candidate function [with D = int, S = std::__1::basic_string<char>] not viable: no known conversion from 'int' to 'int &&' for
      1st argument
    constexpr MyClass(D &&d, S &&s): d_(std::forward<D>(d)), s_(std::forward<S>(s)) {}

Command line: c++ -std=c++17 test.cpp, compiled with Apple clang version 12.0.0 (clang-1200.0.32.6)

It seems the constructor can only accept rvalues. It's not the universal references. So my question is can I achieve the universal reference style constructor? (Also I want to keep MyClass as a template class with the two type parameters D and S.)

2 Answers

In this snippet, d and s are NOT universal references - they are rvalue references (because D and S are already known (deduced) within class by the class instantiation).

 template <typename D, typename S>
    struct MyClass {
        constexpr MyClass(D &&d, S &&s): d_(std::forward<D>(d)), s_(std::forward<S>(s)) {}
    private:
        D d_;
        S s_;
    };

According to Scott Meyers: the type deduction needs to be involved for variables of type D&& and S&& (and in this form only!) for those to be universal references, so you need extra template parameters (so the types are deduced by the class-method(=ctor in this case) instantiation):

template <typename D, typename S>
    struct MyClass {
        template <typename DD, typename SS>
        constexpr MyClass(DD &&d, SS &&s): d_(std::forward<DD>(d)), s_(std::forward<SS>(s)) {}
    private:
        D d_;
        S s_;
    };

Enabling class tempate type deduction via constructor call can be done via class template deduction guide like:

template <typename DD, typename SS> MyClass(DD &&d, SS &&s) -> MyClass<DD, SS>

if we want to initialize a product type just like what you define, we always use aggregate initialization:

MyClass<int, std::string>{ 1, "123"s }; // aggregate initialization won't generate deduction guide automatically until C++20.

and if in C++20 or the deduction guide is provided, we can omit the template arguments:

MyClass{ 1, "123"s }; // after C++20, or by deduction guide.

sometimes, the requirements of aggregate initialization is not satisfied. in this case, passing a copy and then moving it is always cheap for us:

template<typename... Types>
struct A{
    A(Types... args) :members({ std::move(args)... }){};
    std::tuple<Types...> members;
};

but if the moving is expensive as well, to be more efficient, we should use the universal reference:

template<typename... Types>
struct A{
    template<typename... Args>
    A(Args&&... args) :members({ std::forward<Args>(args)... }){};
    std::tuple<Types...> members;
};

However, we lost the type informations from the template arguments of class template, which means we can't use aggregate initialization for the members such as MyClass<std::tuple<int, int>, int>{{1, 2}, 3}.

fortunately, it's easy to solve this problem if it's not variadic template:

template<typename T, typename Y>
struct A{
    template<typename P = T, typename Q = Y>
    A(P&& p, Q&& q) :t(std::forward<P>(p)), y(std::forward<Q>(q)){};
    T t;
    Y y;
};

for variadic template, we have to implement it by TMP ways:

template<typename... Types>
struct DataHelper{
    std::tuple<Types...> memebers;
    DataHelper() = default;
    ~DataHelper() = default;
};

template<typename, typename, typename...>
struct InterfaceHelper;
template<typename Data, typename... Refs>
struct InterfaceHelper<Data, std::tuple<Refs...>> : public virtual Data{
    InterfaceHelper() = default;
    InterfaceHelper(Refs&&... refs) noexcept(noexcept(Data{{std::forward<Refs>(refs)...}})) :Data{{std::forward<Refs>(refs)...}}{}
};
template<typename Data, typename... Refs, typename Head, typename... Tails>
struct InterfaceHelper<Data, std::tuple<Refs...>, Head, Tails...> : public InterfaceHelper<Data, std::tuple<Refs..., Head>, Tails...>, public InterfaceHelper<Data, std::tuple<Refs..., Head const&>, Tails...>{
    InterfaceHelper() = default;
    typedef InterfaceHelper<Data, std::tuple<Refs..., Head>, Tails...> base;
    using base::base;
    typedef InterfaceHelper<Data, std::tuple<Refs..., Head const&>, Tails...> base_cref;
    using base_cref::base_cref;
};
template<typename Data, typename... Refs, typename Head, typename... Tails>
struct InterfaceHelper<Data, std::tuple<Refs...>, Head&, Tails...> : public InterfaceHelper<Data, std::tuple<Refs..., Head&>, Tails...>{
    InterfaceHelper() = default;
    typedef InterfaceHelper<Data, std::tuple<Refs..., Head&>, Tails...> base;
    using base::base;
};
template<typename Data, typename... Refs, typename Head, typename... Tails>
struct InterfaceHelper<Data, std::tuple<Refs...>, Head&&, Tails...> : public InterfaceHelper<Data, std::tuple<Refs..., Head&&>, Tails...>{
    InterfaceHelper() = default;
    typedef InterfaceHelper<Data, std::tuple<Refs..., Head&&>, Tails...> base;
    using base::base;
};

template<typename... Types>
struct Test : private InterfaceHelper<DataHelper<Types...>, std::tuple<>, Types...>{
    ~Test() = default;
    typedef InterfaceHelper<DataHelper<Types...>, std::tuple<>, Types...> base;
    using base::base;
};
template<typename... Args>
Test(Args&&...)->Test<Args...>;

int main(){
    using namespace std::string_literals;
    Test a(1, 2, 3, 4, 5); // works
    std::cout << sizeof(std::tuple<int, int, int, int, int>) << " vs. " << sizeof(a) << std::endl;
    // maybe 20 vs. 280, it costs huge!
}

sadly, it breaks the zero-overhead principle, because there is a huge space cost (O(2^n)) to store the pointer of virtual base class. it's just theoretical.

Related