可变模板构造函数来构建父模板

Variadic Template Constructor to build Parent

本文关键字:构建 构造函数      更新时间:2023-10-16

我尝试从一堆我知道接口的类继承,但是它们可能有非常不同的构造函数。为此,我决定在派生类构造函数中使用可变模板,以便它可以获得任意参数,这些参数最终将提供给父类。

我的代码如下:
#include <iostream>
struct A {
    A(int) { std::cout << "This is intn"; }
    A(unsigned) { std::cout << "This is unsignedn"; }
};
struct B {
    B(char) { std::cout << "This is charn"; }
};
template <typename T>
struct C : public T {
    template <typename... Args>
    C(double, Args&&... params) : T(std::forward<Args>(params)...) { std::cout << "This is doublen"; }
    // But what about this?
    // C(Args&&... params, double) : T(std::forward<Args>(params)...) { std::cout << "This is doublen"; }
};
int main() {
    C<A> p(1.0, 1);
    C<A> r(1.0, 1u);
    C<B> q(1.0, 'c');
    // Which would work as following
    // C<A> p(1,   1.0);
    // C<A> r(1u,  1.0);
    // C<B> q('c', 1.0);
    return 0;
}

我的问题是:

  • 这个代码正确吗?这是我第一次尝试在构造器中使用可变模板,所以如果我错过了什么,我很想听听你的意见。
  • 我更愿意在最后留下子类C的参数,但是据我所知,这是不可能的,因为在构造函数中不允许指定模板参数,在这种情况下,可变参数将吞噬所有参数,不给实际的孩子留下任何参数。有什么办法能做到这一点吗?

14.8.2.1从函数调用中推导模板实参

1[…对于没有出现在参数声明列表末尾的函数参数包,参数包的类型是一个非推导的上下文。

5[…[注意:如果模板参数没有在函数模板的任何函数形参中使用,或者仅在非推导的上下文中使用,则其对应模板参数不能从函数调用中推导出来,必须显式指定模板参数。- 结束说明]

14.8.2.5从类型

推导模板参数

如果模板形参仅在未推导的上下文中使用且未显式指定,则模板实参推导失败。

因此,正如您所看到的,Args...应该被显式指定,但由于这对于构造函数是不可能的,因此这将不起作用。

的作用是将用于基构造函数的所有参数打包到一个元组中(实际示例),后面跟着派生类的剩余参数:

template <typename T>
class C : public T
{
    template<typename... A, size_t... I>
    C(std::tuple<A...>&& a, sizes<I...>, double x) :
        T(std::get<I>(std::move(a))...) { std::cout << "This is doublen"; }
public:
    template<typename... A>
    C(std::tuple<A...>&& a, double x) :
        C(std::move(a), idx<sizeof...(A)>{}, x) { }
};

用作

int main() {
    C<A> p(std::forward_as_tuple(1),      1.0);
    C<A> r(std::forward_as_tuple(1u, 2.), 1.0);
    C<B> q(std::forward_as_tuple('c', 0), 1.0);
}

我使你的例子更丰富:

struct A {
    A(int) { std::cout << "This is intn"; }
    A(unsigned, double) { std::cout << "This is unsigned, doublen"; }
};
struct B {
    B(char, int) { std::cout << "This is char, intn"; }
};

和剩余的样板

template<size_t... I> struct sizes { using type = sizes<I...>; };
template<size_t N, size_t K = 0, size_t... I>
struct idx_t : idx_t<N, K+1, I..., K> {};
template<size_t N, size_t... I>
struct idx_t<N, N, I...> : sizes<I...> {};
template<size_t N>
using idx = typename idx_t<N>::type;

只在std::integer_sequence可用之前存在。

如果你发现std::forward_as_tuple的名字太长了,那么定义你自己的名字并不难,例如pack:

template<typename... A>
constexpr std::tuple<A&&...>
pack(A&&... a) { return std::tuple<A&&...>{std::forward<A>(a)...}; }

尽管如此,语法

C<B> q(pack('c', 0), 1.0);

确实增加了一点开销,但我发现最终用户很自然地知道发生了什么。如果向派生类构造函数中添加更多的参数,那么扁平化到C<B> q('c', 0, 1.0);不仅几乎不可能实现,而且还会使用户产生歧义和混淆。真正酷的是像

这样的语法
C<B> q('c', 0; 1.0);

(也可以在函数中分隔输入/输出参数),但我没有在任何语言中见过这个,只在教科书中见过。