Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I resolve this ambiguous template constructor call?

Tags:

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?

like image 568
Franko Leon Tokalić Avatar asked Mar 22 '16 14:03

Franko Leon Tokalić


1 Answers

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;
}
like image 195
Barry Avatar answered Oct 11 '22 14:10

Barry