Daniel Langr
Daniel Langr

Reputation: 23527

Why deduction guide for std::array does not allow different types?

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

Upvotes: 7

Views: 1060

Answers (3)

Caleth
Caleth

Reputation: 63142

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

Upvotes: 6

Frank Boyne
Frank Boyne

Reputation: 4570

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.

Upvotes: 1

T.C.
T.C.

Reputation: 137415

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).

Upvotes: 10

Related Questions