接受括号初始化列表并推断长度的数组类

时间:2018-11-08 08:09:01

标签: c++ c++17 initializer-list template-deduction c++20

以前曾有人问过这个问题,但我很好奇,看看新的C ++标准是否有任何变化。任何当前或将来的标准都是可以接受的。

Q :是否仍然可以创建一个可以使用braced-init-list初始化的Array类,而无需手动指定数组长度以及存储在堆栈中的元素,并且不需要'make_array'函数。

template<class T, size_t N>
struct Array
{
    T items[N];
};

Array<int> foo = { 1, 2, 3 };

由于initializer_list并未以大小为模板,因此使用它的构造函数将无法完成工作。 C ++ 17 几乎中的推导指南可以工作,但是您必须省略type参数,并且所有项目都必须具有完全相同的类型

Array foo = { 1, 2, 3 }; // Works
Array<int> foo = { 1, 2, 3 }; // Doesn't work
Array foo = { 1.0, 2.0, 3.0f }; //Doesn't work

采用c数组的构造函数似乎无效,因为initializer_list不会转换为c数组。

braced-init-list中发生的T[N]int foo[] = { 1, 2, 3 };是纯粹的编译器魔术,无法在代码中复制吗?

编辑:这个问题的实质是关于上面的 exact 语法。没有make_array,没有额外的模板参数,显式的项目类型,没有双括号,没有动态分配。如果琐碎的数组需要大量现代C ++伪造知识,但仍然无法支持标准语法,那么我认为这只是一个不好的工程折衷。

5 个答案:

答案 0 :(得分:7)

您可以使用明确的推导指南来解决列表中所有类型都相同的需求:

template <class... T>
Array(T&&... t) -> Array<std::common_type_t<T...>, sizeof...(T)>;

Array foo = { 1.0, 2.0, 3.0f }; // Deduces Array<double,3u>

答案 1 :(得分:5)

我不愿成为坏消息的承担者,但我认为,当前(至少从C ++ 17开始),您的问题的答案是“不”,不可能满足您的所有要求给定要求。就是说,这里发布的解决方案很接近,但是都不能满足您的一个或多个要求。

我还怀疑答案在不久的将来会保持“否”。不是说我是先知,而是最近几个C ++版本的方向似乎暗示着make_array解决方案更有可能是所添加的,而不是更直接的语言支持。

说明

请允许我详细解释原因。

首先考虑C ++ 17演绎指南。我不会详细介绍它们,因为对此问题的其他答案已适当讨论了它们。它们确实非常接近您的要求,但似乎在某种方面还是不足。 (尽管@ max66的答案似乎可以满足您的所有要求,除了多余的括号。如果他的语法实际上起作用,您可能会认为该答案“足够接近”。)

下一步考虑一种可变模板解决方案。为了自动确定N,您将需要一系列重载函数(基本上是一个带有一个自变量的函数,一个带有一个自变量和其余变量模板的函数)。但这实际上等同于某种形式的make_array,因此也不算。

最后,我能看到的唯一其他选择是基于initializer_list。问题是如何从该列表中确定N。在C ++ 11中,这显然是不可能的,因为对列表大小的唯一访问是const而不是constexpr。但是,从C ++ 14开始,size()方法实际上是constexpr,因此您认为至少从理论上讲,使编译器可以以此推论N。不幸的是,您需要将N(模板参数)默认设置为类构造函数(初始化列表)中的某个值。我无法确定以当前语言形式进行此操作的任何方法。

我认为将来的版本可能会对此提供支持

一种支持这种方法的方法是遵循其他语言的示例并添加直接语言支持,将语法桥接到某些类。但这实际上使某些类成为“特殊”类。考虑一下Swift中的以下行:

let ar = [1, 2, 3, 4]

在此示例中,arArray<Int>类型的对象。但这是通过直接编译器支持完成的,即“数组”是一种特殊情况。无论您做什么,都无法编写执行相同方式的MyArray类(除了让MyArray接受Array作为构造选项之外)。当然可以扩展C ++标准以执行类似的操作,但是C ++倾向于尝试避免那些“特殊”情况。此外,可以提出一个论点

auto ar = make_array(1, 2, 3, 4);
实际上,

比以下内容更清晰地表示了意图:(建议使用字符'A'将其与初始值设定项列表区分开)

auto ar = A{ 1, 2, 3, 4 };

另一种方法(更符合当前C ++语法)是将N参数添加到initializer_list模板类中。毕竟size()现在是constexpr,所以必须在编译时知道其大小,那么为什么不将其用作模板参数呢?它可以适当地默认设置,因此几乎不需要它,但是它将允许类(std::array等标准类和您提议的自定义类)将Array模板中的N绑定到N中的initializer_list。然后,您应该可以按照以下几行写一些东西:

template<class T, size_t N>
struct Array 
{
    explicit Array(std::initializer_list<N> il);
}

当然,诀窍是以一种不会破坏大量现有代码的方式来进行此initializer_list更改。

我怀疑标准委员会不会采用上述任何一种方法,而是会添加实验性make_array方法。而且我不认为这是一个坏主意。我们已经习惯了make_...语言的许多其他部分,所以为什么不在这里呢?

答案 2 :(得分:1)

  

C ++ 17中的推导指南几乎可以使用,但是您必须省略type参数,并且所有项目都必须具有完全相同的类型

不一定。

是的,您不能显式地指定type参数,但是可以根据项目的类型来决定。

我想出两种合理的策略:(1)Array的类型是第一项的类型,遵循std::array的方式,因此编写以下推导指南

template <typename T, typename ... Us>
Array(T, Us...) -> Array<T, 1u + sizeof...(Us)>; 

(但是请注意,对于UsT类型不同于std::array的C ++程序格式错误)或(2)遵循了metalfox的建议并选择项目的常见类型

template <typename ... Ts>
Array(Ts...) -> Array<std::common_type_t<Ts...>, sizeof...(Ts)>;
  

在纯T[N]中发生的foo[] = { 1, 2, 3 };的括号初始列表是否纯粹是无法在代码中复制的编译器魔术?

您是否在考虑以下扣减指南?

template <typename T, std::size_t N>
Array(T const (&)[N]) -> Array<T, N>;

有效,但有一些缺点:(1)您必须使用它添加几个括号

//    added ---V         V--- added
Array foo1 = { { 1, 2, 3 } }; // Works

和(2)仍然是所有项目必须具有相同类型的问题

Array foo2 = { {1.0, 2.0, 3.0f} }; //Doesn't work: incompatible types

否则编译器无法推断类型T

P.s .: make_array()函数有什么问题?

P.s.2:我建议看一下BoBTFish的答案,以了解一种很好的方法来绕过不可能使用演绎指南来显式表示模板参数的情况。

答案 3 :(得分:1)

这是一种允许您指定类型的方法。我们使用一个额外的参数来指定类型,以便我们仍然可以使用推导指南来选择尺寸。我认为它不是很漂亮:

#include <iostream>

template <typename T>
struct Tag { };

template <typename T, size_t N>
struct Array {
    T data_[N];

    template <typename... U>
    Array(Tag<T>, U... u)
      : data_{static_cast<T>(u)...} // cast to shut up narrowing conversions - bad idea??
    {}
};

template <typename T, typename... U>
Array(Tag<T>, U...) -> Array<T, sizeof...(U)>;

int main()
{
    Array a{Tag<double>{}, 1, 2.0f, 3.0};
    for (auto d : a.data_) {
        std::cout << d << '\n';
    }
}

显然,这不是此类的完整实现,只是为了说明该技术。

答案 4 :(得分:0)

您已经问过:

  

Q:无论如何,是否可以创建一个可以用braced-init-list初始化的Array类,而无需手动指定数组长度,也不需要'make_array'函数。

我已经完成了一个类的实现,该类的行为与您描述的方式相同,只是它有点复杂,但仍然相当简单,可读,可重用,可移植且通用。

我无法将T items[]数组作为类的直接成员。我不得不改用T* items并在派生类中创建一个重载的operator[]来模仿数组的行为。这并不意味着没有其他人所展示的解决方案。我只是发现这是一种可能的解决方案,而无需指定数组的大小。

我使用基类通过std::initializer_listvariadic constructor存储构造函数中的元素。类模板本身不是可变参数模板,只有其构造函数才是。基类将initializer_listparameter pack中的值存储到std::vector中。通过调用向量类的vector函数,继承的类将T*的内容存储到data()中。

template<typename T>
class ParamPack {
protected:
    std::vector<T> values_;
    size_t size_;
public:
    template<typename... U>
    ParamPack( U... u ) : 
      values_{ static_cast<T>(u)... }, 
      size_( sizeof...(U) ) {}

    template<typename ... U>
    ParamPack( std::initializer_list<std::is_same<T, U...>( U...)> il ) : 
      values_( il ), size_( il.size() ) {}

    std::vector<T>& operator()() { return values_; }

    size_t size() const { return size_; }
};    

template<typename T>
class Array : public ParamPack<T> {
private:
    T* items_;
public:
    template<typename... U>
    Array( U... u ) : ParamPack<T>::ParamPack( u... ) {
        items_ = this->values_.data();
    }

    template<typename... U>
    Array( std::initializer_list<U...> il ) : ParamPack<T>::ParamPack( il ) {
        items_ = this->values_.data();
    }

    T& operator[]( size_t idx ) {
        return items_[idx];
    }

    T operator[]( size_t idx ) const {
        return items_[idx];
    }

    T* data() const { return items_; }
};

int main() {
    try {
        // Parameter Pack Examples:
        // Variadic Constructor { ... }
        std::cout << "ParamPack<T> Examples:\n";
        std::cout << "Using ParamPack<T>'s Variadic Constructor\n";
        ParamPack<int> pp1( 1, 2, 3, 4  );
        std::cout << "Size: " << pp1.size() << " | Elements: ";
        for( auto& v : pp1() ) {
            std::cout << v << " ";
        }
        std::cout << '\n';      

        std::cout << "Using ParamPack<T>'s Variadic Constructor with an Initializer List\n";
        ParamPack<int> pp2( { 5, 6, 7, 8 } );
        std::cout << "Size: " << pp2.size() << " | Elements: ";
        for( auto& v : pp2() ) {
            std::cout << v << " ";
        }
        std::cout << '\n';

        std::cout << "Using ParamPack<T>'s initializer_list constructor\n";
        std::initializer_list<int> il{ 9,10,11,12 };
        ParamPack<int> pp3( il );       
        std::cout << "Size: " << pp3.size() << " | Elements: ";
        for( auto& v : pp3() ) {
            std::cout << v << " ";
        }
        std::cout << "\n\n";    

        // Array Examples:
        std::cout << "Array<T> Examples:\n";
        std::cout << "Using Array<T>'s initializer_list Constructor\n";
        Array<int> arr( il );       
        for( size_t i = 0; i < arr.size(); i++ ) {
            std::cout << arr[i] << " ";
        }
        std::cout << "\n";

        // Using Variadic Constructor
        std::cout << "Using Array<T>'s Variadic Constructor\n";
        Array<int> testA( 9, 8, 7, 6 );
        for( size_t i = 0; i < testA.size(); i++ ) {
            std::cout << testA[i] << " ";
        }
        std::cout << '\n';

        Array<std::string> testB( "Hello", " World" );
        for( size_t i = 0; i < testB.size(); i++ ) {
            std::cout << testB[i] << " ";
        }
        std::cout << "\n\n";

        // Using Constructor w/ Initializer List
        std::cout << "Using Array<T>'s Variadic Constructor with Initializer List\n";
        Array<int> testC( { 105, 210, 420 } );
        for( size_t i = 0; i < testC.size(); i++ ) {
            std::cout << testC[i] << " ";
        }
        std::cout << "\n\n";

        // Using Initializer List with =
        std::cout << "Using Array<T>'s Initializer List with =\n";
        Array<int> a = { 1, 2, 3, 4 };
        for( size_t i = 0; i < a.size(); i++ ) {
            std::cout << a[i] << " ";
        }
        std::cout << '\n';                

        Array<char> b = { 'a', 'b', 'c', 'd' };
        for ( size_t i = 0; i < b.size(); i++ ) {
            std::cout << b[i] << " ";
        }
        std::cout << '\n';

        Array<double> c = { 1.2, 3.4, 4.5, 6.7 };
        for( size_t i = 0; i < c.size(); i++ ) {
            std::cout << c[i] << " ";
        }
        std::cout << "\n\n"; 

        // Using Initializer List directly
        std::cout << "Using Array<T>'s Initalizer List directly\n";
        Array<uint32_t> a1{ 3, 6, 9, 12 };
        for( size_t i = 0; i < a1.size(); i++ ) {
            std::cout << a1[i] << " ";
        }
        std::cout << "\n\n";

        // Using user defined data type
        struct Point {
            int x_, y_;
            Point( int x, int y ) : x_( x ), y_( y ) {}
        };
        Point p1( 1, 2 ), p2( 3, 4 ), p3( 5, 6 );

        // Variadic Constructor
        std::cout << "Using Array<T>'s Variadic Consturctor with user data type\n";
        Array<Point> d1( p1, p2, p3 );
        for( size_t i = 0; i < d1.size(); i++ ) {
            std::cout << "(" << d1[i].x_ << "," << d1[i].y_ << ") ";
        }
        std::cout << '\n';

        // Initializer List Construtor (reversed order)
        std::cout << "Using Array<T>'s Initializer List Constructor with user data type\n";
        Array<Point> d2( { p3, p2, p1 } );
        for( size_t i = 0; i < d2.size(); i++ ) {
            std::cout << "(" << d2[i].x_ << "," << d2[i].y_ << ") ";
        }
        std::cout << '\n';

        // Initializer List Version = {...} p2 first
        std::cout << "Using Array<T>'s  = Initializer List with user data type\n";
        Array<Point> d3 = { p2, p1, p3 };
        for( size_t i = 0; i < d3.size(); i++ ) {
            std::cout << "(" << d3[i].x_ << "," << d3[i].y_ << ") ";
        }
        std::cout << '\n';

        // Initializer List Directly p2 first p1 & p3 swapped
        std::cout << "Using Array<T>'s Initializer List directly with user data type\n";
        Array<Point> d4{ p2, p3, p1 };
        for( size_t i = 0; i < d4.size(); i++ ) {
            std::cout << "(" << d4[i].x_ << "," << d4[i].y_ << ") ";
        }
        std::cout << '\n';  

        std::initializer_list<Point> ilPoints{ p1, p2, p3 };
        std::cout << "Using Array<T>'s initializer_list Constructor with user data type\n";
        Array<Point> d5( ilPoints );
        for( size_t i = 0; i < d5.size(); i++ ) {
            std::cout << "(" << d5[i].x_ << "," << d5[i].y_ << ") ";
        }
        std::cout << "\n\n";    

        // Need a local copy of the vector instead?
        std::cout << "Using Array<T>'s base class's operator()() to retrieve vector\n";
        std::vector<Point> points = d4(); // using operator()()
        for( auto& p : points ) {
            std::cout << "(" << p.x_ << "," << p.y_ << ") ";
        }
        std::cout << '\n';

        // Need a local copy of the pointer instead?
        std::cout << "Using Array<T>'s data() to get the contents of its internal pointer\n";
        Point* pPoint = nullptr;
        pPoint = d4.data();
        for( size_t i = 0; i < d4.size(); i++ ) {
            std::cout << "(" << pPoint[i].x_ << "," << pPoint[i].y_ << ") ";
        }
        std::cout << '\n';

    } catch( const std::runtime_error& e ) {
        std::cerr << e.what() << '\n';
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

-输出-

ParamPack<T> Examples:
Using ParamPack<T>'s Variadic Constructor
Size: 4 | Elements: 1 2 3 4
Using ParamPack<T>'s Variadic Constructor with an Initializer List
Size: 4 | Elements: 5 6 7 8 
Using ParamPack<T>'s initializer_list Constructor
Size: 4 | Elements: 9 10 11 12

Array<T> Examples:
Using Array<T>'s initializer_list Constructor
9 10 12 12

Using Array<T>'s Variadic Constructor
9 8 7 6
Hello World

Using Array<T>'s Constructor with Initializer List
105 210 420

Using Array<T>'s Initializer List with =
1 2 3 4
a b c d
1.2 3.4 5.6 7.8

Using Array<T>'s Initializer List directly
3 6 9 12

Using Array<T>'s Variadic Constructor with user data type
(1,2) (3,4) (5,6)
Using Array<T>'s Variadic Constructor With Initializer List of user data type
(5,6) (3,4) (1,2)
Using Array<T>'s = Initializer List with user data type
(3,4) (1,2) (5,6)
Using Array<T>'s Initializer List directly with user data type
(3,4) (5,6) (1,2)
Using Array<T>'s initializer_list Constructor with user data type

Using Array<T>'s base class's operator()() to retrieve vector
(3,4) (5,6) (1,2)   
Using Array<T>'s data() to get the contents of its internal pointer
(3,4) (5,6) (1,2)

现在,它有了一些健壮的功能,因为它具有可从父类和子类中使用的运算符,可以从父类中直接从其vector获取存储的operator()()。在子类中,您可以从operator[]()索引到子存储的指针,并且有一个函数可以返回其大小。模板本身不包含size_t N模板参数,因为其大小内部存储在基类中,并由其向量的大小确定。以此,我将T* p视为T p[size]。此类仍然不是没有一些限制。

-有效构造-

Array<int> a( 1, 2, 3, 4 ); // Variadic Constructor Okay
Array<int> a( {1,2,3,4} ); // Initializer List Constructor Okay
Array<int> a = { 1, 2, 3, 4 }; // Initializer List Okay
Array<int> a{ 1,2,3,4 }; // Initializer List Okay 

-限制-

但是您必须explicitly实例化模板,因为它们都将产生编译器错误,因此它们将无法工作。

Array a( 1,2,3,4 );
Array a( {1,2,3,4} );
Array a = { 1, 2, 3, 4 }; 
Array a{ 1,2,3,4 };

-注意-可以做更多的事情来提高效率,甚至线程安全或异常安全,但这只是类设计的概括。


请告诉我您对此的看法: