2

I'm writing a templated container class and want constructors to differentiate between a single value to initialize all elements in the container (which is OK) and an array of values (whose size must match the size specified in the template instantiation).

Everything works great unless I specify a brace-initialized parameter of just one element for a template instance with SIZE > 1; unfortunately doing so picks the first constructor, and if the template is instantiated with SIZE > 1 but yet the user provided a brace-initialized parameter (array) of only one element, I want to catch that error with a static_assert.

Here's a stripped-down version of my code that shows the problem:

#include <iostream>

template <typename TYPE, size_t SIZE>
class Array
{
public:
   // CTOR #1: Initialize EACH ELEMENT from a single TYPE initializer
   explicit Array(TYPE const value)
   {
      std::cout << "CTOR #1, SIZE=" << SIZE << "\n";
      for (size_t i = 0; i < SIZE; ++i)
         m_buf[i] = value;
   }

   // CTOR #2: Initialize from a C-style array of the same size
   template <size_t N>
   explicit Array(TYPE const (&values)[N])
   {
      static_assert(N == SIZE, "CTOR #2: Attempt to initialize from a C-style array of the wrong size.");
      std::cout << "CTOR #2, SIZE=" << SIZE << ", N=" << N << "\n";
      for (size_t i = 0; i < SIZE; ++i)
         m_buf[i] = values[i];
   }

   template <typename T, size_t S>
   friend std::ostream& operator<<(std::ostream&, Array<T, S> const&);

private:
   TYPE m_buf[SIZE];
};

template <typename TYPE, size_t SIZE>
std::ostream& operator<<(std::ostream& os, Array<TYPE, SIZE> const& arr)
{
   for (size_t i = 0; i < SIZE; ++i)
      os << ' ' << arr.m_buf[i];
   return os;
}

int main()
{
   int CStyleArray1[1] = { 1 };
   int CStyleArray6[6] = { 1, 2, 3, 4, 5, 6 };
   int CStyleArray7[7] = { 1, 2, 3, 4, 5, 6, 7 };
   int CStyleArray8[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
   (void)CStyleArray6; (void)CStyleArray8; // avoid unused variable warnings

   Array<int, 7>  a(5);             // ctor #1

 //Array<int, 7>  b(CStyleArray6);  // ctor #2 fails due to static_assert(N==SIZE) (this is good!)
   Array<int, 7>  c(CStyleArray7);  // ctor #2
 //Array<int, 7>  d(CStyleArray8);  // ctor #2 fails due to static_assert(N==SIZE) (this is good!)

 //Array<int, 7>  e({ 1, 2, 3, 4, 5, 6});       // ctor #2 fails due to static_assert(N==SIZE) (this is good!)
   Array<int, 7>  f({ 1, 2, 3, 4, 5, 6, 7});    // ctor #2
 //Array<int, 7>  g({ 1, 2, 3, 4, 5, 6, 7, 8}); // ctor #2 fails due to static_assert(N==SIZE) (this is good!)

   Array<int, 1>  h(CStyleArray1);  // ctor #2
   Array<int, 1>  i({1});           // ctor #1 (why did this not use ctor #2 with N=1?)

 //Array<int, 7>  j(CStyleArray1);  // ctor #2 fails due to static_assert(N==SIZE) (this is good!)
   Array<int, 7>  k({1});           // ctor #1 (why did this not use ctor #2 with N=1, and fail the static_assert?)

   std::cout <<   " a:" << a
           //<< "\n b:" << b
             << "\n c:" << c
           //<< "\n d:" << d
           //<< "\n e:" << e
             << "\n f:" << f
           //<< "\n g:" << g
             << "\n h:" << h
             << "\n i:" << i
           //<< "\n j:" << j
             << "\n k:" << k
             << std::endl;
}

When I compile & run that, this is the output:

C:\GitHOME\test>g++ -O3 -std=gnu++11 -g -Wall -Werror test.cpp

C:\GitHOME\test>a.exe
CTOR #1, SIZE=7
CTOR #2, SIZE=7, N=7
CTOR #2, SIZE=7, N=7
CTOR #2, SIZE=1, N=1
CTOR #1, SIZE=1
CTOR #1, SIZE=7
 a: 5 5 5 5 5 5 5
 c: 1 2 3 4 5 6 7
 f: 1 2 3 4 5 6 7
 h: 1
 i: 1
 k: 1 1 1 1 1 1 1   <------ I expected this to use ctor #2 and fail the static_assert!

C:\GitHOME\test>

Is there any way I can get code like Array<int, 7> k({1}); to call ctor #2 so it triggers the static_assert? I'd like a brace-initialized list of one element to be considered an array of size [1], not considered a single value.

phonetagger
  • 7,701
  • 3
  • 31
  • 55
  • Did you try to overload the ctor for `std::initializer_list` ?.... Ah, that won't help as `std::initializer_list` does not have a compile time size data member – Dmytro Ovdiienko Jun 22 '22 at 21:57
  • @DmytroOvdiienko If catching the mismatch with `static_assert` isn't possible, a runtime `assert` would be better than not catching the problem at all. Would `std::initializer_list` work and let me catch the problem with a runtime `assert`? I've never used `std::initializer_list` before. – phonetagger Jun 22 '22 at 22:06
  • 1
    `{1}` isn't an array. consider the line `int x = { 1 };` Did i just assign an array to an int? https://en.cppreference.com/w/c/language/scalar_initialization – Taekahn Jun 22 '22 at 22:09
  • _Would std::initializer_list work and let me catch the problem with a runtime assert_: Yes, you can call `assert(std::size(l) == SIZE);` – Dmytro Ovdiienko Jun 22 '22 at 22:13
  • 2
    BTW, I believe it is not really good idea to have a conversion ctor with one argument. You will get exactly the same problem as `vector{2} vs vector(2)`. The first case constructs a vector with **one** element of "2" while the second constructs the vector of defaulted constructed **two** elements. Consider adding a factory function which constructs the container and fills it with the given value – Dmytro Ovdiienko Jun 22 '22 at 22:24
  • See also https://stackoverflow.com/questions/5438671/static-assert-on-initializer-listsize – Dmytro Ovdiienko Jun 22 '22 at 22:28

0 Answers0