Класс массива, который будет принимать список фигурных скобок и определять длину - PullRequest
0 голосов
/ 08 ноября 2018

Об этом уже спрашивали, но мне любопытно посмотреть, изменилось ли что-нибудь в новых стандартах C ++. Любой текущий или будущий стандарт является приемлемым.

Q : Есть ли в любом случае создать класс Array, который можно инициализировать с помощью braced-init-list без необходимости вручную указывать длину массива, с элементами, хранящимися в стеке, и не требуется функция '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 };, чисто магией компилятора, которая не может быть воспроизведена в коде?

РЕДАКТИРОВАТЬ: Суть этого вопроса о синтаксисе точный выше. Нет make_array, нет дополнительного аргумента шаблона, явный тип элемента, нет двойных скобок, нет динамического выделения. Если для тривиального массива требуется куча современного дурака C ++ и он все еще не может поддерживать стандартный синтаксис, то, на мой взгляд, это просто плохой компромисс в инженерном деле.

Ответы [ 5 ]

0 голосов
/ 10 ноября 2018

Вы спрашивали это:

В: В любом случае, можно ли создать класс Array, который можно инициализировать с помощью фигурного списка инициализации, без необходимости вручную указывать длину массива и без функции 'make_array'.

Я работал над реализацией класса, который ведет себя так, как вы описываете, только он немного сложнее, но все еще довольно простой, читаемый, многоразовый, переносимый и универсальный.

Мне не удалось получить массив T items[] в качестве прямого члена в классе. Вместо этого мне пришлось использовать T* items и создать перегруженный operator[] в производном классе, чтобы имитировать поведение массива. Это не означает, что здесь нет работы, как показали другие. Я просто нахожу, что это одно из возможных решений без указания размера массива.

Я использую базовый класс для хранения элементов из конструктора с помощью std::initializer_list или variadic constructor. Сами шаблоны классов не являются вариационными, только их конструктор. Базовый класс хранит значения из initializer_list или parameter 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;
}

-Output-

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 

-Limitations-

Однако вы должны 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 };

-Note- Может быть, можно сделать больше, чтобы сделать это более эффективным, возможно, даже поточно-ориентированным или безопасным для исключений, но это всего лишь обобщение дизайна класса.


Пожалуйста, дайте мне знать, что вы думаете по этому поводу:

0 голосов
/ 08 ноября 2018

Вот один подход, который позволяет вам указать тип.Мы используем дополнительный аргумент для указания типа, так что мы все еще можем использовать руководство по выводу, чтобы подобрать размер.Хотя я не считаю это привлекательным:

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

Это явно не полная реализация такого класса, просто чтобы проиллюстрировать методику.

0 голосов
/ 08 ноября 2018

Вы можете обойти необходимость, чтобы все типы в списке были одинаковыми, с помощью явного руководства по выводам:

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>
0 голосов
/ 08 ноября 2018

Руководства по дедукции в C ++ 17 почти работают, но вы должны опустить параметр type, и все элементы должны иметь точно такой же тип

Не обязательно.

Да, вы не можете явно указать параметр типа, но вы можете выбрать его в соответствии с типами элементов.

Я представляю две разумные стратегии: (1) тип Array является типом первого элемента, следующим образом std::array, поэтому пишу следующее руководство по выводу

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

(но обратите внимание, что программа C ++, в которой тип Us отличается от T, для std::array плохо сформирована) или (2) следует предложению метафокса и выбирает общий тип элемента

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

Является ли фигурный список инициализации для T[N], что происходит в int 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, чтобы увидеть хороший метод, позволяющий обойти невозможность явного аргумента шаблона с помощью руководств по выводам.

0 голосов
/ 08 ноября 2018

Я ненавижу быть носителем плохих новостей, но я верю, что в настоящее время (по крайней мере, в 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]

В этом примере ar является объектом типа Array<Int>. Но это делается прямой поддержкой компилятора, то есть «массив» является особым случаем. Независимо от того, что вы делаете, вы не можете написать класс MyArray, который будет работать таким же образом (за исключением того, что MyArray, возможно, примет Array в качестве варианта построения). Конечно, для стандарта C ++ возможно расширение до чего-то похожего, но C ++ старается избегать этих «особых» случаев. Кроме того, можно привести аргумент, что

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

на самом деле является более четким представлением намерения, чем было бы что-то вроде следующего: (предлагая символ «А», чтобы отличить это от списка инициализатора)

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

Другим способом , более соответствующим текущему синтаксису C ++, было бы добавить параметр N в класс шаблона initializer_list. В конце концов, поскольку size() теперь constexpr, размер должен быть известен во время компиляции, так почему бы не сделать его доступным в качестве параметра шаблона? Он может подходящим образом по умолчанию, так что он может понадобиться редко, но он позволит классам (как стандартным, таким как std::array, так и пользовательским, которые вы предлагаете), возможность связать N в шаблоне Array с N в initializer_list. Тогда вы сможете написать что-то вроде следующего:

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

Конечно, уловка заключается в том, чтобы сделать это initializer_list изменение таким способом, который не нарушает существующий код.

Я подозреваю, что комитет по стандартам не пойдет ни по одному из этих путей, но скорее добавит экспериментальный метод make_array.И я не уверен, что это плохая идея.Мы привыкли к make_... во многих других частях языка, так почему бы и не здесь?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...