5

I have a matrix class template:

#include <iostream>
#include <array>
#include <initializer_list>
#include <utility>
#include <type_traits>
#include <cstddef>

enum ColumnFill {
    COLUMNS
};

template <typename T, std::size_t M, std::size_t N>
struct TMatrixMxN {
    TMatrixMxN(T x = T(0)) {
        std::cout << "Default" << std::endl;
    }

    TMatrixMxN(std::initializer_list<T> values) {
        std::cout << "Row initializer" << std::endl;
    }

    TMatrixMxN(std::initializer_list<T> values, ColumnFill dummy) {
        std::cout << "Column initializer" << std::endl;
    }

    TMatrixMxN(std::initializer_list<std::initializer_list<T>> values) {
        std::cout << "Value initializer" << std::endl;
    }

    TMatrixMxN(const std::array<std::array<T, N>, M> &values) {
        std::cout << "From array" << std::endl;
    }

    TMatrixMxN(const TMatrixMxN<T, M - 1, N - 1> &x) {
        std::cout << "From lower dimension" << std::endl;
    }

    TMatrixMxN(const TMatrixMxN &x) {
        std::cout << "Copy" << std::endl;
    }

    TMatrixMxN(TMatrixMxN &&x) {
        std::cout << "Move" << std::endl;
    }
};

typedef TMatrixMxN<float, 1, 1> Matrix1x1;
typedef TMatrixMxN<float, 2, 2> Matrix2x2;
typedef TMatrixMxN<float, 3, 3> Matrix3x3;
typedef TMatrixMxN<float, 3, 1> Matrix3x1;

Everything's dandy until I use a matrix that has 1 in any of its dimensions:

int main() {
    std::array<std::array<float, 3>, 3> arr{{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}};
    Matrix3x3 m1;
    Matrix3x3 m2({1, 2, 3});
    Matrix3x3 m3({1, 2, 3}, COLUMNS);
    Matrix3x3 m4({{1, 2, 3}, {4, 5, 6}, {7, 8, 9}});
    Matrix3x3 m5(arr);
    Matrix3x3 m6(Matrix2x2({{1, 2}, {3, 4}}));
    Matrix3x3 m7(m6);
    Matrix3x3 m8(std::move(m7));

    std::cout << std::endl;

    TMatrixMxN<float, 3, 2>({{1, 2}, {3, 4}, {5, 6}});

    std::cout << std::endl;

    // PROBLEMS:
    Matrix3x1({{1}, {2}, {3}});  // error: ambiguous
    Matrix1x1({{1}});            // error: ambiguous
}

I don't know how to nicely solve this problem (I want those calls to call the value-initializer constructor)

When compiled with g++ -std=c++11 Ambiguous.cpp, the compiler thinks the following constructors are candidates for 3x1 matrix: move, copy, from-lower-dimension, value-initializer, row-initializer. For 1x1 matrix it also lists from-array and default.

Things I've tried:

  • Using SFINAE to make an overload condition that T has to be an arithmetic type (std::is_arithmetic), because I thought that it sometimes might think that it's an initializer_list, but it changed nothing. I realized that it's a useless check since it actually already knows good and well that T is float in this example
  • Adding an explicit keyword to these constructors: value-initializer, row-initializer, column-initializer, from-array, and from-lower-dimension, because I thought there are some implicit calls going on, but it also didn't change anything
  • Making a typedef "fil" (as "float initializer list") and converting the code to {fil{1}}, since I realized that braced stuff might not be interpreted as an initializer list - then it worked as intended. However I don't think that this fix is okay enough.

Can it be done at all?

Franko Leon Tokalić
  • 1,457
  • 3
  • 22
  • 28

1 Answers1

4

The ambiguity in the single-dimensional case is between these two constructors:

TMatrixMxN(std::initializer_list<T> values);
TMatrixMxN(std::initializer_list<std::initializer_list<T>> values)

Because {{1}, {2}, {3}} can also be interpreted as an overly aggressively braced version of {1, 2, 3}. Note that it's not the size N or M that matters, this still fails:

Matrix3x3 m9({{1}, {2}, {3}}); // same error

The simplest solution is to disable the ambiguity by turning the less-desired constructor into a constructor template with dummy parameters:

template <size_t _=M>
TMatrixMxN(std::initializer_list<T> values) {
    std::cout << "Row initializer" << std::endl;
}

We didn't actually change anything - we just made this a template. Now, if both constructors match, the initializer_list<initializer_list<T>> one will be preferred since it's not a template. If you want to actually disable this one for M==1 or N==1, you can add an enable_if_t into there too:

template <size_t m=M, size_t n=N, class = std::enable_if_t<(m != 1 && n != 1)>>
TMatrixMxN(std::initializer_list<T> values) {
    std::cout << "Row initializer" << std::endl;
}
Barry
  • 286,269
  • 29
  • 621
  • 977
  • Thank you! :) Could you please show me how could I do that with enable_if_t though? – Franko Leon Tokalić Mar 22 '16 at 15:29
  • I see, I did the same except that I didn't add the new template parameters (m=M, n=N) and just used M and N but that didn't work.. could you explain why do I need new template parameters? – Franko Leon Tokalić Mar 22 '16 at 15:46
  • 1
    @ComradeBearabyte SFINAE only works in immediate context of substitution... the outer template parameters aren't in the immediate context of deduction. I'm sure there are answers on here that explain this in more depth. – Barry Mar 22 '16 at 15:47