Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Array class that will accept an braced-init-list and deduce length

This has been asked before, but I'm curious to see if anything has changed in newer C++ standards. Any current or future standard is acceptable.

Q: Is there anyway to create an Array class that can be initialized with a braced-init-list without having to manually specify the array length, with elements stored on the stack, and doesn't require a 'make_array' function.

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

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

Since initializer_list is not templated on the size, a constructor using it won't do the job. Deduction guides in C++17 nearly work, but you have to omit the type parameter and all items must have exactly the same 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

A constructor that takes a c-array doesn't appear to work because an initializer_list won't convert to a c-array.

Is the braced-init-list to T[N] that happens in int foo[] = { 1, 2, 3 }; purely compiler magic that can't be replicated in code?

EDIT: The spirit of this question is about the exact syntax above. No make_array, no extra template argument, explicit item type, no double braces, no dynamic allocations. If a trivial Array requires a bunch of modern C++ tomfoolery and still can't manage to support standard syntax then it's just a bad engineering trade-off in my opinion.

like image 724
Adam Avatar asked Nov 08 '18 08:11

Adam


4 Answers

You can work around the need for all the types in the list to be the same by using an explicit deduction guide:

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>
like image 68
metalfox Avatar answered Oct 28 '22 16:10

metalfox


I hate to be the bearer of bad news, but I believe that at the current time (at least as of C++17) the answer to your question is "no" there is no way that would meet all of your given requirements. That said, there are solutions posted here that come close, but they all fall short in one or more of your requirements.

I also suspect that the answer will remain "no" in the near future. Not that I'm a prophet, but the direction of the last few C++ versions would seem to imply that a make_array solution is more likely to be what is added rather than more direct language support.

Explanation

Allow me to explain why in a little more detail.

First consider the C++17 deduction guides. I won't go into detail on them as they are discussed suitably by other answers on this question. They do come very close to what you are asking but do seem to fall short in one way or the other. (Although the answer by @max66 would seem to meet all your requirements except for the extra brackets. If his syntax in fact works you may want to consider that answer as "close enough".)

Next consider a variatic templates solution. In order to automatically determine N you would need a series of overloaded functions (basically one with a single argument and one that takes a single argument and the rest of the variatic templates). But that would essentially be equivalent to some form of make_array so it doesn't count either.

Finally, the only other option I could see would be based on an initializer_list. The question is how to determine N from that list. In C++11 this would be obviously impossible since the only access to the size of the list is const and not constexpr. However as of C++14 the size() method is in fact constexpr so you would think it is at least theoretically possible to get the compiler to deduce N based on that. Unfortunately to do this you would need to make N (a template parameter) default to a value of something in the class constructor (the initializer list). I have been unable to determine any means of doing this in the present form of the language.

How I think a future version could support this

One way to support this would be to follow the examples of other languages and add direct language support, bridging a syntax to certain classes. But this essentially makes some classes "special". Consider the following line in Swift:

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

In this example ar is an object of the type Array<Int>. But this is done by direct compiler support, i.e. "Array" is a special case. No matter what you do you could not write a MyArray class that would perform the same way (other than perhaps having MyArray accept an Array as a construction option). It is certainly possible for the C++ standard to be extended to do something similar, but C++ tends to try to avoid those "special" cases. Besides, an argument could be made that

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

is in fact a clearer representation of the intent than would be something like the following: (suggesting the character 'A' to distinguish this from an initializer list)

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

Another way, more in line with current C++ syntax, to do this would be to add the N parameter to the initializer_list template class. After all since size() is now constexpr the size must be known at compile time, so why not make it available as a template parameter? It could suitably default so that it could rarely be needed, but it would allow classes (both standard ones like std::array and custom ones like you are proposing) the possibility of tying the N in the Array template to the N in the initializer_list. Then you should be able to write something along the following lines:

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

Of course the trick would be to make this initializer_list change in a fashion that does not break a lot of existing code.

I suspect the standards committee will not follow either of these paths, but will more likely add the experimental make_array method. And I'm not convinced that is a bad idea. We are used to make_... in many other parts of the language, so why not here as well?

like image 34
Steven W. Klassen Avatar answered Oct 28 '22 18:10

Steven W. Klassen


Here's one approach that allows you to specify the type. We use an extra argument to specify the type, so that we can still use the deduction guide to pick up the size. I don't consider it pretty though:

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

This is clearly not a full implementation of such a class, just to illustrate the technique.

like image 1
BoBTFish Avatar answered Oct 28 '22 18:10

BoBTFish


Deduction guides in C++17 nearly work, but you have to omit the type parameter and all items must have exactly the same type

Not necessarily.

Yes, you can't explicit the type parameter, but you can decide it according the types of the items.

I imagine two reasonable strategies: (1) the type of the Array is the type of the first item, following the std::array way, so writing the following deduction guide

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

(but observe that a C++ program where a Us type is different from T, for a std::array, is ill formed) or (2) follows the metalfox's suggestion and select the item's common type

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

Is the braced-init-list to T[N] that happens in int foo[] = { 1, 2, 3 }; purely compiler magic that can't be replicated in code?

Are you thinking in a deduction guide as follows ?

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

Works but with a couple of drawbacks: (1) you have to add a couple of brackets using it

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

and (2) remain the problem that all items must have the same type

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

or the compiler can't deduce the type T

P.s.: what's wrong with a make_array() function ?

P.s.2: I suggest to give a look at BoBTFish's answer to see a nice method to bypass the impossibility to explicit a template argument using deduction guides.

like image 1
max66 Avatar answered Oct 28 '22 18:10

max66