TheHelpfulHelper
TheHelpfulHelper

Reputation: 77

How do I solve the ambiguity of brace-elisioning with list-initializers in C++?

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?

Upvotes: 4

Views: 521

Answers (2)

davidbak
davidbak

Reputation: 5999

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

Upvotes: 0

TheHelpfulHelper
TheHelpfulHelper

Reputation: 77

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.

Upvotes: 1

Related Questions