4

I want to make my own struct for a 3x3 Matrix. I want to allow construction via components/elements or by "rows".

So either you provide a std::array<float, 9> or a std::array<std::array<float, 3>, 3>

However when defining the struct like this with the following constructors:

struct Matrix3x3
{
    Matrix3x3(std::array<float, 9> components) { }

    Matrix3x3(std::array<std::array<float, 3>, 3> rows) { }
};

Then the second constructor is ambiguos with the first. Meaning that you can call the second constructor like this

Matrix3x3{ {{ {{1.f, 2.f, 3.f}}, {{4.f, 5.f, 6.f}}, {{7.f, 8.f, 9.f}} }} };

without any problems, however calling the first constructor like this

Matrix3x3{ {{1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f}} };

will give the following message and error:

message : No constructor could take the source type, or constructor overload resolution was ambiguous
error C2440: '<function-style-cast>': cannot convert from 'initializer list' to 'ArrayTest'

And Visual Studio tells me that there is "more than one instance of constructor ... matches the argument list".

I tested doing the same thing but with an array of ints( because its easier to work with) and a length of one. Consider:

struct ArrayTest
{
    ArrayTest(std::array<int, 1> arrayOfInts) { }

    ArrayTest(std::array<std::array<int, 1>, 1> arrayOfArraysOfInts) { }
};

Then the first 3 are valid and compile for the first constructor, while all 5 of them compile for the second constructor.

auto test1 = ArrayTest{ {1} };

auto test2 = ArrayTest{ { {1} } };

auto test3 = ArrayTest{ { { {1} } } };

auto test4 = ArrayTest{ { { { {1} } } } };

auto test5 = ArrayTest{ { { { { {1} } } } } };

For the simple array constructor "test3" is the full and complete initialization, where the first pair of brackets initializes the aggregate ArrayTest, the second pair initializes the array, the third pair initializes the first element of that array and finally the fourth bracket initializes the integer 1 which is rarely seen, but valid. "test1" and "test2" are then just brace-elisioned versions of "test3".

For the array-of-arrays constructor it is similar where "test5" is the complete initialization and all others are brace-elisioned. This is what causes the ambiguity.

So the question is: How do I solve this problem? Or is there a better way/solution?

  • Does this answer your question? [When can outer braces be omitted in an initializer list?](https://stackoverflow.com/questions/11734861/when-can-outer-braces-be-omitted-in-an-initializer-list) – jtbandes Feb 20 '21 at 19:30
  • @jtbandes No unfortunately it does not, or at least Im not understanding how it would. This question is not about why and when you can omit braces in initilization, but why my two constructors are ambiguous. – TheHelpfulHelper Feb 20 '21 at 19:41
  • @TheHelpfulHelper: It's ambiguous because both constructors could be called given that set of braces. And brace elision is what makes it possible for either constructor to be called given that set of braces. – Nicol Bolas Feb 20 '21 at 20:05
  • @NicolBolas I have reconsidered, and redefined my problem. I understand why it is ambiguous now, but I still dont know how to solve this problem elegantly. – TheHelpfulHelper Feb 20 '21 at 21:18
  • Why doesn't just _eliminating_ one or the other constructor solve your problem ... elegantly? – davidbak Feb 20 '21 at 22:27
  • @davidbak I want the flexibility of being able to construct a matrix by component, and by rows. And that works when they are not provided via list initializers, as shown in my own answer. However when both constructors exist, then you cannot list initialize a matrix just by its components, since that causes an ambiguity. – TheHelpfulHelper Feb 20 '21 at 22:33
  • What if you just provide the type in the constructor call like this?: `Matrix3x3{ std::array{1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f} };` – ssbssa Feb 20 '21 at 22:48
  • @ssbssa Yes, that would solve the ambiguity. Not very elegantly however I think, since you have to temporarily construct an array that you then pass into the constructor of the matrix, but it does work. The point of list-initialization afaik is to avoid unnessecary temporary constructions. – TheHelpfulHelper Feb 20 '21 at 22:52
  • 1
    List-initialization uses list initializer objects. It doesn't avoid the temporary. Also, while passing std::array as argument should be trivial to optimize for compiler, I wouldn't be so sure about passing list initializer. I'm pretty sure it's content will need to be copied to array. – S. Kaczor Feb 20 '21 at 22:55
  • @S.Kaczor Can you refer me to some more detailed information or evidence about this? I would love to understand the optimizations the compiler (GCC) makes for this specific case. – TheHelpfulHelper Feb 20 '21 at 23:04
  • For evidence try it out on Godbolt.org. – davidbak Feb 20 '21 at 23:06

2 Answers2

1

One way to work around this is to seperately declare and define temporary arrays like this:

std::array<float, 9> components{ 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f };

std::array<float, 3> row1{ 1.f, 2.f, 3.f };
std::array<float, 3> row2{ 4.f, 5.f, 6.f };
std::array<float, 3> row3{ 7.f, 8.f, 9.f };

Matrix3x3 m1{ components };
Matrix3x3 m2{ { row1, row2, row3} };

This avoids the ambiguity of brace-elisioning with list-initializers, however this is rather tedious and does not seem "optimal", since then you're constructing temporary arrays, just to construct something else, while the point of list-initialization in this case would be to avoid those.

1

Use a tag: An empty struct, e.g., struct by_row{};. Then your by-row constructor takes that as first argument, the other does not. Similar to how the algorithms in <algorithm> take an "execution policy" as first argument to distinguish the parallel versions from the sequential versions.

struct by_row {};

struct Matrix3x3
{
    Matrix3x3(std::array<float, 9> components) { ... }

    Matrix3x3(by_row, std::array<std::array<float, 3>, 3> rows) { ... }
};

Call looks like:

    Matrix3x3 m{1,0,0,0,1,0,0,0,1};
    Matrix3x3 n{by_row{}, {{{1,0,0},{0,1,0},{0,0,1}}}};

(Weird extra braces explained in the question linked in one of the comments above.)

(Godbolt) (Actually, I like this version even better.)

davidbak
  • 5,775
  • 3
  • 34
  • 50