【问题标题】:Ensuring template argument type matches that of its variadic constructor确保模板参数类型与其可变参数构造函数匹配
【发布时间】:2019-05-26 06:35:21
【问题描述】:

我想要这样的课程:

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

我还想让构造函数检查以确保传递给构造函数的每个参数都是&lt;T&gt; 类型;简单地说,每个Ts 必须是T

我可能想多了,但对于我的生活,我无法得到这个或类似的东西来编译。我不知道它是在enable_ifis_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“正在初始化,无法从初始化列表转换为容器,没有构造函数可以采用源类型,或者构造函数重载决议不明确。”

【问题讨论】:

    标签: c++ templates c++17 variadic-functions enable-if


    【解决方案1】:

    为什么不直接使用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”从“double”缩小到“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); ^ 注意:候选:'template 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 并进行所有额外的工作:delete[] 等...

    【讨论】:

    • 这适用于初始化列表,但构造函数呢?我还说我没有使用内部容器,例如矢量,我使用的是原始指针!
    • @FrancisCugler 好的,抱歉,我没有看到您的 f(1,2,3..) 要求。等一下,我会考虑的
    • 我知道我可以一直使用vector,但我不在这个特殊情况下,因为它是自学的并且是为了演示目的。我的问题更多是关于让enable_ifis_same 正常工作,以便我的构造函数可以是可变参数,而模板本身并不能确保传递给构造函数的每种类型都是相同的类型的模板。然后就是将构造函数参数中的各个内容存储到该类型的内部指针中......
    • @FrancisCugler 我明白没问题。只是在内存泄漏等方面使用唯一或共享 ptr 更安全......
    • ...我知道我没有为临时人员保留内存,因为我必须在某些时候使用 new 和 delete 。我也有兴趣使用类的构造函数的初始化列表来初始化指向这些值的指针。
    【解决方案2】:

    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)...}
        {
        }
    };
    

    Live Demo

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

    【讨论】:

      【解决方案3】:

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

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

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

      现在关于语法:

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

      您可以查看here 以了解您的类似方法。

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

      #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;
      }
      

      【讨论】:

      • 感谢enable_ifis_same的解释。我知道我可以轻松使用vector,但这不是我想要的。我说这只是自我教育,并不实用。我确实认为我没有为必须在某处使用 new 和 delete 的临时对象分配内存,但更多的是让 enable_if 和 is_same 工作,以便构造函数可以是可变参数,而模板本身不是。在这种情况下,我有理由不使用矢量;它仅用于演示目的。
      【解决方案4】:

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

      另外,通过 const-rvalue-ref 传递数据是没有意义的,因为不可能从 const 对象中移动数据。另外,您正在将参数包移动到指针。我不知道该怎么办,所以就把它删了。

      另外,看看std::decay_t - 如果没有它,它将无法工作,因为有时Ts 不是int,而是const int &amp;int &amp;。这导致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_{ } {}
       };
      

      【讨论】:

        猜你喜欢
        • 2018-05-18
        • 1970-01-01
        • 2023-04-02
        • 1970-01-01
        • 2017-10-31
        • 2015-10-06
        相关资源
        最近更新 更多