7

The deduction guide for std::array requires all types be the same:

std::array arr = { 1, 2, 3.4 }; // error

What is the rationale behind such a requirement? Would there be any significant drawback if different types were allowed instead? For example:

namespace std {
   template <typename... T>
   array(T...) -> array<std::common_type_t<T...>, sizeof...(T)>;
}

std::array arr = { 1, 2, 3.4 }; // decltype(arr)::value_type deduced as double
Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
  • Then what errors are you getting? Are you sure it's a problem with the *type* deduction and not the *size* deduction? Or perhaps it's a problem with the compiler? What compilers are you testing with? What versions of those compilers? – Some programmer dude Jun 20 '18 at 11:15
  • 1
    @Someprogrammerdude It's not a problem, it's a matter of choice. And I wonder whether there is some rationale behind the choice that has been made. – Daniel Langr Jun 20 '18 at 11:15
  • 6
    I think it makes perfect sense for values used for array initialization to have the same type because array holds elements of the same type. While implicit conversion of integer to double in your example looks like a potential shot in the leg. – user7860670 Jun 20 '18 at 11:17
  • Consider you have classes `A`, and `B`, which can be implicitly converted from one to another, due to relevant conversion constructors being present. Which type (`A`, or `B`), would you expect `std::array` to choose, when constructing the array, of instances, of both, then? – Algirdas Preidžius Jun 20 '18 at 11:21
  • Also indexing of such an error would seem a major pain in the butt to me. Every element could be of different size. How would you perform random access? By first calculating the offset by iterating every element up until the target element? – Neijwiert Jun 20 '18 at 11:23
  • 3
    @AlgirdasPreidžius The same type that `std::common_type` derives, if any. – Daniel Langr Jun 20 '18 at 11:25
  • @Neijwiert You did't understand the question. Of course, the array would have all the elements of the same type. Just initialization with elements of different type would be allowed together with deduction of template arguments. – Daniel Langr Jun 20 '18 at 11:26
  • 2
    @AlgirdasPreidžius - A compiler error would be expected (and produced thanks to `common_type`). Frankly, so long as it's not a narrowing conversion. I don't see an issue. – StoryTeller - Unslander Monica Jun 20 '18 at 11:28
  • @StoryTeller I suspected, that there might be a compiler error involved, but I just wanted to raise a question, about the expectations, when conversions are present. In addition, what about another case, where you have classes `A`, `B`, and `C`. `A` can be converted to `B`, and `B` can be converted to `C`. `C` is seemingly the common type, since everything can be, directly, or indirectly, converted to it, hence such deduction still fails due to the fact, that no more than one implicit conversion is done, for classes. – Algirdas Preidžius Jun 20 '18 at 11:44
  • @AlgirdasPreidžius, is implicit conversion transitive? – Joseph D. Jun 20 '18 at 11:46
  • 3
    @AlgirdasPreidžius - Yeah, but I don't really see a failing conversion as a problem. If it fails due to conversions, then the programmer will need to be more explicit, as they already need to be. It's just a bit annoying to require the same explicitness when the compiler is able to figure it out. Just merely having the deduction guide is a step in that direction. This question is focused on why a second step wasn't taken as well. – StoryTeller - Unslander Monica Jun 20 '18 at 11:47
  • 2
    @StoryTeller Hmm.. So, it is not about solving all of the corner cases, but rather, deducing as much as possible, whenever it is.. Yeah, I can see your point. – Algirdas Preidžius Jun 20 '18 at 11:50
  • 3
    The increase in complexity is huge, with the payoff being you type slightly less. I'd think that's enough reason. As a reference, read [std::common_type](http://en.cppreference.com/w/cpp/types/common_type), and see how long it takes. – Passer By Jun 20 '18 at 12:10
  • `std::array` is not special. Other containers do not accept list initializer of different types too. – xskxzr Jun 20 '18 at 16:41
  • You already know why! Some maeutics: which type to you think should hold the array? And if the float was the first parameter? – Oliv Jun 22 '18 at 18:44

3 Answers3

10

There are substantial design issues with using common_type. For example, std::common_type_t<A, B, C>, std::common_type_t<C, A, B> and std::common_type_t<C, B, A> need not all exist - and if they do, need not be the same type:

struct A;
struct B;
struct C;
struct A { operator B(); };
struct B { operator C(); };
struct C { operator A(); };

static_assert(std::is_same_v<std::common_type_t<A, B, C>, C>);
static_assert(std::is_same_v<std::common_type_t<C, A, B>, B>);
static_assert(std::is_same_v<std::common_type_t<C, B, A>, A>);

That makes for an "interesting" user experience when reordering the elements of the initializer causes a different type to be deduced (or an error to be emitted).

T.C.
  • 133,968
  • 17
  • 288
  • 421
6

It matches how function template arguments are deduced.

e.g.

template<typename T>
void foo(T, T){}

template<typename T>
struct bar{ bar(T, T) {} };

int main()
{
    foo(1, 1.5); // error, note:   deduced conflicting types for parameter 'T' ('int' and 'double')
    bar(1, 1.5); // error, note:   deduced conflicting types for parameter 'T' ('int' and 'double')
}

But you can provide a deduction guide for common types.

template<typename T>
struct baz{ baz(T, T) {} };

template<typename T, typename U>
baz(T, U) -> baz<std::common_type_t<T, U>>    

or overloads that forward to common types

template<typename T>
void quux(T, T){}

template<typename T, typename U>
std::enable_if_t<!std::is_same<std::decay_t<T>, std::decay_t<U>>> quux(T t, U u) 
{ 
    using C = std::common_type_t<T, U>; 
    quux<C>(std::forward<C>(t), std::forward<C>(u)); // I think this is right
}

int main()
{
    baz(1, 1.5);
    quux(1, 1.5);
}
Caleth
  • 52,200
  • 2
  • 44
  • 75
  • I can even provide a deduction guide for `std::array`: https://wandbox.org/permlink/av80vxqPSIpdW4DO. Though, it's technically undefined behavior ;-) – Daniel Langr Jun 20 '18 at 12:39
  • @DanielLangr You could also overload `foo` (carefully excluding `T == U`) to forward to `foo`, and have equally UB introducing similar overloads into `namespace std` – Caleth Jun 20 '18 at 12:46
1

Edit: In a comment below, Daniel points out he was really asking about why a std::array of one single type could not be initialized by a list containing entries of different types (for example two int and a double). Not why a std::array has to be one single type.

It doesn't address Daniel's request for a rationale but some C++ environments support the experimental\array header and the std::experimental::make_array class which does allow initialisation of a std::array from different types. The array type can be deduced or specified...

#include <array>
#include <experimental/array>

// creates array of doubles 
auto arr = std::experimental::make_array(1, 2, 3.4);

// creates array of ints
auto ra = std::experimental::make_array<int> (1, 2, 3.4);

ideone runnable sample


What is the rationale behind such a requirement?

The restriction is required to meet the semantics of the array container.

For example, cppreference puts it like this …

This container is an aggregate type with the same semantics as a struct holding a C-style array T[N] as its only non-static data member.

Obviously there's no way to have a C-style array of more than one type T.

For another perspective: cplusplus says this …

Container properties

Sequence
Elements in sequence containers are ordered in a strict linear sequence. Individual elements are accessed by their position in this sequence.

Contiguous storage
The elements are stored in contiguous memory locations, allowing constant time random access to elements. Pointers to an element can be offset to access other elements.

Fixed-size aggregate
The container uses implicit constructors and destructors to allocate the required space statically. Its size is compile-time constant. No memory or time overhead.

For pointer arithmetic to make sense every element has to be the same width. For dereferencing the pointer (after pointer arithmetic) to make sense each element has to use the same bit representation - otherwise you might find yourself treating an IEEE floating point representation as if it were a two's-complement integer (or vice versa).


It doesn't really address your request for a rationale to say "because the standard says so" but it does (I think). I can't find a citeable copy of the actual standard but a working draft of the C++ standard said this...

26.3.7.2 array constructors, copy, and assignment [array.cons]
The conditions for an aggregate (11.6.1) shall be met. Class array relies on the implicitly-declared special member functions (15.1, 15.4, and 15.8) to conform to the container requirements table in 26.2. In addition to the requirements specified in the container requirements table, the implicit move constructor and move assignment operator for array require that T be MoveConstructible or MoveAssignable, respectively.

template<class T, class... U>  
  array(T, U...) -> array<T, 1 + sizeof...(U)>;`   

Requires: (is_same_v<T, U> && ...) is true. Otherwise the program is ill-formed.

Frank Boyne
  • 4,400
  • 23
  • 30
  • 2
    You likely misinterpreted my question. I didn't ask about a possibility to create an array that would contain elements of different types. I asked about possibility to create an array of elements of the same type such that 1) this type would be deduced and 2) aggregate initialization list could have elements of different types. – Daniel Langr Jun 21 '18 at 06:00
  • I certainly did, sorry about that! Can I suggest you edit your question to add that clarification? It might help others avoid my mistake. – Frank Boyne Jun 21 '18 at 15:16