确保模板参数类型与其可变参数构造函数的类型匹配

Ensuring template argument type matches that of its variadic constructor

本文关键字:类型 参数 构造函数 变参 确保      更新时间:2023-10-16

我想有一个这样的类:

template<typename T>
struct Foo {
T* data_;
template<typename... Ts, std::enable_if<std::is_same<T,Ts>...>...>
explicit Foo(Ts...ts) : data_{ ts... } {}
};

但是;语法有些错误,我不确定是否可以在初始化时像这样直接将参数设置为指针。

我想做的只是:

Foo<int> f1{ 1, 3, 5, 7 }; // Or
// Foo<int> f1( 1, 3, 5 7 );
// f1.data_[0] = 1
// f1.data_[1] = 3
// f1.data_[2] = 5
// f1.data_[3] = 7
// f1.data_[4] = ... not our memory either garbage or undefined...
Foo<float> f2{ 3.5f, 7.2f, 9.8f }; // Or
// Foo<float> f2( 3.5f, 7.2f, 9.8f );
// f2.data_[0] = 3.5
// f2.data_[1] = 7.2
// f2.data_[2] = 9.8
// f2.data_[3] = ... not our memory

我还想让构造函数检查以确保传递到构造函数中的每个参数都是<T>类型;只需为每个Ts输入它必须是T

我可能想多了,但对于我的生活,我无法编译这个或类似的东西。我不知道它是在enable_if内,is_same还是通过类的初始值设定项列表并尝试将内容存储到指针中。我不知道我是否应该改用T数组,但在将参数传递到构造函数之前,数组的大小是未知的。我也在尝试不使用基本容器(例如std::vector)来执行此操作;它更多的是自我教育,而不是实用的源代码。我只是想看看如何使用原始指针完成此操作。


编辑

我把我的班级改成这样:

template<typename T>
struct Foo {
T* data_;
template<typename... Ts, std::enable_if_t<std::is_same<T, Ts...>::value>* = nullptr>
explicit Foo( const Ts&&... ts ) : data_{ std::move(ts)... } {}
};

尝试使用它时:

int a = 1, b = 3, c = 5, d = 7;
Foo<int> f1( a, b, c, d );
Foo<int> f2{ a, b, c, d };

我更接近这个迭代;但它们都给出了不同的编译器错误。

  • 第一个是:C2661: "没有重载函数需要 4 个参数">
  • 第二个:C2440:"初始化,无法从初始值设定项列表转换为容器,没有构造函数可以采用源类型,或者构造函数重载解析不明确。

为什么不简单地使用std::initialize_list:

#include <iostream>
#include <type_traits>
#include <vector>
template <class T>
struct Foo
{
std::vector<T> data_;
explicit Foo(std::initializer_list<T> data) : data_(data)
{
std::cout << "1";
};
template <typename... Ts,
typename ENABLE=std::enable_if_t<(std::is_same_v<T,Ts> && ...)> >
explicit Foo(Ts... ts) : Foo(std::initializer_list<T>{ts...})
{
std::cout << "2";
}
};
int main()
{
Foo<int> f1{1, 3, 5, 7}; // prints 1
Foo<int> f2(1, 3, 5, 7); // prints 1 then 2
return 0;
}

如果某些TsT不同,则会收到编译时错误。

gcc -std=c++17  prog.cpp  

你会得到:

Foo<int> f1{1, 3, 5., 7};

错误:将"5.0e+0"从"双精度"到"int"的转换范围缩小到内部 { } [-Wnarrowing] Foo f1{1, 3, 5., 7}; ^

Foo<int> f2(1, 3, 5., 7);

你得到

错误:调用 'Foo::Foo(int, int, double, int)' Foo f2(1, 3, 5., 7); ^ 注:候选者:'模板Foo::Foo(Ts ...)' 显式 Foo(Ts... ts) : Foo(std::initializer_list{ts...})

更新:如果你真的想使用像原始指针这样的东西,这里有一个完整的工作示例:

#include <iostream>
#include <memory>
#include <type_traits>
#include <vector>
template <class T>
struct Foo
{
size_t n_;
std::unique_ptr<T[]> data_;
explicit Foo(std::initializer_list<T> data) : n_(data.size()), data_(new T[n_])
{
std::copy(data.begin(), data.end(), data_.get());
std::cout << "1";
};
template <typename... Ts, typename ENABLE = std::enable_if_t<(std::is_same_v<T, Ts> && ...)> >
explicit Foo(Ts... ts) : Foo(std::initializer_list<T>{ts...})
{
std::cout << "2";
}
friend std::ostream& operator<<(std::ostream& out, const Foo<T>& toPrint)
{
for (size_t i = 0; i < toPrint.n_; i++)
std::cout << "n" << toPrint.data_[i];
return out;
}
};
int main()
{
Foo<int> f1{1, 3, 5, 7};  // prints 1
Foo<int> f2(1, 3, 5, 7);  // prints 1,2
std::cout << f1;
std::cout << f2;
return 0;
}

我让你用原始指针替换unique_ptr,并完成所有额外的工作:删除[]等...

>std::is_same只比较两种类型,并且不能使用包扩展来声明多个模板参数。 这意味着您需要将所有std::is_same检查拉出到另一个检查中:

template <typename T, typename... Ts>
struct all_same : std::bool_constant<(std::is_same<T, Ts>::value && ...)> {};
template <typename T>
struct Foo
{
std::vector<T> data_;
template <typename... Ts, std::enable_if_t<all_same<T, std::decay_t<Ts>...>::value>* = nullptr>
Foo(Ts&&... ts)
: data_{std::forward<Ts>(ts)...}
{
}
};

现场演示

您还需要为data_阵列分配内存。 在这里,我用std::vector为我处理分配,但如果你真的愿意,你可以使用new[]delete[]自己管理它。

enable_ifis_same不会在任何地方存储任何内容,它们只是编译时构造,不会屈服于二进制可执行文件中的任何代码。

无论语法如何,您的代码本质上所做的是尝试获取构造函数参数的地址(这是一个临时参数)。一旦构造函数退出,这将是一个悬空的指针。

要么Foo拥有内存区域,必须在构造函数中分配并在析构函数中删除(如果有疑问:使用std::vector!),要么它为一些外部存储器提供别名,并且必须接收指向该内存的指针。

现在关于语法:

  • std::is_same是一个模板,它提供了一个value布尔常量,可以这样使用:std::is_same<T1, T2>::value。或者,您可以使用std::is_same_v<T1, T2>.
  • std::enable_if仅当
  • 常量表达式(第一个模板参数)为 true 时才提供type类型成员。像std::enable_if<expr, T>::type一样使用它.如果expr为真,则type是要T的类型定义。否则,它未定义并产生替换失败。或者,您可以使用std::enable_if_t<expr, T>

你可以在这里看看你的类似方法。

但您也可以通过使用成员向量来简化所有这些。在这种情况下,向量构造函数确保所有参数都具有兼容的类型。下面是一个完整的示例:

#include <string>
#include <vector>
#include <iostream>
#include <type_traits>
using namespace std;
template<typename T>
struct Foo {
vector<T> data_;
template<typename ...Ts>
explicit Foo(Ts... ts) : data_{ ts... } {}
void print() {
for (const auto &v : data_) {
cout << v << " ";
}
cout << endl;
}
};
int main() {
Foo<int> ints { 1, 2, 3, 4, 5 };
Foo<string> strings { "a", "b", "c", "d", "e"};
// Foo<string> incorrect { "a", 2, "c", 4, "e"};
ints.print();
strings.print();
// incorrect.print();
return 0;
}

在 C++17 中执行此操作的最惯用方法是使用std::cunjunction_v。它懒惰地计算后续值,并允许您避免折叠表达式。此外,编译器生成的消息对于您在编辑的代码段中提到的两种情况都是相同的。

此外,通过 const-rvalue-ref 传递数据是没有意义的,因为不可能从 const 对象移动数据。此外,您正在将一堆论点移动到指针上。我不知道该怎么办,所以只是删除了它。

此外,看看std::decay_t- 没有它,它将无法工作,因为有时Ts不是被推断为int而是const int &int &。这导致std::is_same_v是假的。

下面的代码在 godbolt 上编译得很好:

template<typename T>
struct Foo {
T* data_;
template<
typename... Ts,
std::enable_if_t<
std::conjunction_v<
std::is_same<T, std::decay_t<Ts>>...
>
> * = nullptr
>
explicit Foo( Ts&&... ts ) : data_{ } {}
};