Home > Net >  ctor is ambiguous between single and multiple std::initializer_list ctors on clang and gcc but not m
ctor is ambiguous between single and multiple std::initializer_list ctors on clang and gcc but not m

Time:08-30

I have a nested initializer_list ctor for creating 2D matrixes. Works great. But then I decided to add a simplified row vector matrix (n rows, 1 col) using a single initializer list. This is so I could create a row matrix like this: Matrix2D<int> x{1,2,3} instead of having to do this: Matrix2D<int> x{{1},{2},{3}}. Of course this required a separate ctor.

Everything worked including in constexpr with MSVC. But when checked using gcc and clang I'm getting ambiguous ctors. Seems pretty clear to me when the lists are nested and MSVC operates exactly as expected.

Here's the code:

// Comment out next line for only nested initializer_list ctor
#define INCLUDE_EXTRA_CTOR
#include <memory>
#include <numeric>
#include <initializer_list>
#include <exception>
#include <stdexcept>
#include <concepts>

using std::size_t;
template <typename T>
class Matrix2D {
    T* const pv;    // pointer to matrix contents
public:
    const size_t cols;
    const size_t rows;

    // default ctor;
    Matrix2D() noexcept : pv(nullptr), cols(0), rows(0) {}

    // 2D List initialized ctor
    Matrix2D(std::initializer_list<std::initializer_list<T>> list) :
        pv((list.begin())->size() != 0 ? new T[list.size() * (list.begin())->size()] : nullptr),
        cols(pv != nullptr ? (list.begin())->size() : 0),
        rows(pv != nullptr ? list.size() : 0)
    {
        if (pv == nullptr)
            return;
        for (size_t row = 0; row < list.size(); row  )
        {
            if (cols != (list.begin()   row)->size())
                throw std::runtime_error("number of columns in each row must be the same");
            for (size_t col = 0; col < cols; col  )
                pv[cols * row   col] = *((list.begin()   row)->begin()   col);
        }
    }
#ifdef INCLUDE_EXTRA_CTOR
    // Row initialized ctor, rows=n, cols=1;
    Matrix2D(std::initializer_list<T> list) :
        pv(list.size() != 0 ? new T[list.size()] : nullptr),
        cols(pv != nullptr ? 1 : 0),
        rows(pv != nullptr ? list.size() : 0)
    {
        if (pv == nullptr)
            return;
        for (size_t row = 0; row < rows; row  )
        {
            pv[row] = *(list.begin()   row);
        }
    }
#endif

    // dtor
    ~Matrix2D() { delete[] pv; }
};

int main()
{
    // Tests of various possible null declarations 
    Matrix2D<int> x1{ };        // default
    Matrix2D<int> x2{ {} };     // E0309, nested init list with 1 row, 0 cols, forced to 0 rows, 0 cols
    Matrix2D<int> x3{ {},{} };  // E0309, nested init list with 2 rows, 0 cols, forced to 0 rows, 0 cols

    // typical declaration
    Matrix2D<int> x4{ {1,2},{3,4},{5,6} };  // nested init list with 3 rows, 2 cols
    // standard row vector declaration
    Matrix2D<int> x5{ {1},{2},{3} };  // E0309, init list with 3 rows, 1 col

#ifdef INCLUDE_EXTRA_CTOR
    // row vector declaration
    Matrix2D<int> x6{ 1,2,3 };  // init list with 3 rows, 1 col
#endif
}

E0309 is the MSVC intellisense ambiguous ctor error. However, compiles w/o error Why are gcc and clang deductions ambiguous? Is there a workaround?

Compiler Explorer

CodePudding user response:

However, compiles w/o error Why are gcc and clang deductions ambiguous?

Ambiguous arises here because {} or {1} can also initialize a single int.

Is there a workaround?

Template your specialized constructor such that {} is never deduced to initializer_list which still works for {1,2,3}.

#ifdef INCLUDE_EXTRA_CTOR
    // Row initialized ctor, rows=n, cols=1;
    template<class U>
    Matrix2D(std::initializer_list<U> list) :
        pv(list.size() != 0 ? new T[list.size()] : nullptr),
        cols(pv != nullptr ? 1 : 0),
        rows(pv != nullptr ? list.size() : 0)
    {
        fmt::print("1: {}\n", list);
        if (pv == nullptr)
            return;
        for (size_t row = 0; row < rows; row  )
        {
            pv[row] = *(list.begin()   row);
        }
    }
#endif

If you want Matrix2D<double> d1{ 1,2,4. } to work, then you can use type_identity_t to establish non-deduced contexts in template argument deduction:

// Row initialized ctor, rows=n, cols=1;
template<class U = T>
Matrix2D(std::initializer_list<std::type_identity_t<U>> list) :
    pv(list.size() != 0 ? new T[list.size()] : nullptr),
    cols(pv != nullptr ? 1 : 0),
    rows(pv != nullptr ? list.size() : 0)
{

Demo

CodePudding user response:

If we examine the compiler error:

<source>:62:29: error: call of overloaded 'Matrix2D(<brace-enclosed initializer list>)' is ambiguous
   62 |     Matrix2D<int> x3{ {},{} };  // nested init list with 2 rows, 0 cols, forced to 0 rows, 0 cols
      |                             ^
<source>:39:5: note: candidate: 'Matrix2D<T>::Matrix2D(std::initializer_list<_Tp>) [with T = int]'
   39 |     Matrix2D(std::initializer_list<T> list) :
      |     ^~~~~~~~
<source>:22:5: note: candidate: 'Matrix2D<T>::Matrix2D(std::initializer_list<std::initializer_list<_Tp> >) [with T = int]'
   22 |     Matrix2D(std::initializer_list<std::initializer_list<T>> list) :
      |     ^~~~~~~~

then it kinda tells us with <brace-enclosed initializer list> that it doesn't recognize neither the initializer_list<T>, nor initializer_list<initializer_list<T>>. Unfortunately, initializer_list is a "runtime" construct: its size is not known at compile time.

initializer_list has an additional bad property: it matches better than copy or move contructor, so if you accidentally use list initialization when you intended to copy or move-initialize your object, the error will be very confusing :)

Instead, if you know your matrix layout at compile time, you can use some templating with std::index_sequence and passing arrays to constructor. If you don't, you can still use std::initializer_list and specify the sizes as additional arguments.

Here is an excerpt of what I wrote for myself some time ago trying to solve a similar problem (godbolt):

#include <array>
#include <cstddef>
#include <cstring>
#include <type_traits>
#include <utility>

template <std::size_t, typename T>
using enumerate = T;

template <typename Precision, typename NthInnerArrayIndexSequence,
          std::size_t InnerDimension>
class MatrixImpl;

template <typename Precision, std::size_t... NthInnerArrayPack,
          std::size_t InnerDimension>
class MatrixImpl<Precision, std::index_sequence<NthInnerArrayPack...>,
                 InnerDimension> {
   public:
    MatrixImpl() = default;

    // Initialization is row-major. Every inner array is a row.
    constexpr explicit MatrixImpl(
        enumerate<
            NthInnerArrayPack,
            Precision const (&)[InnerDimension]>... nth_inner_array) noexcept {
        // memcpy() is more efficient, but it is not constexpr.
        if (std::is_constant_evaluated()) {
            ((insert(mat_, nth_inner_array, NthInnerArrayPack)), ...);
        } else {
            ((std::memcpy(mat_.data()   NthInnerArrayPack * InnerDimension,
                          nth_inner_array, sizeof nth_inner_array)),
             ...);
        }
    }

   protected:
    std::array<Precision, sizeof...(NthInnerArrayPack) * InnerDimension> mat_;

   private:
    constexpr void insert(decltype(mat_)& dst,
                          Precision const (&src)[InnerDimension],
                          std::size_t nth_pack) noexcept {
        for (size_t i{nth_pack * InnerDimension}, j{0UL};
             i < nth_pack * InnerDimension   InnerDimension;   i,   j)
            dst[i] = src[j];
    }
};

// Base class with routines for any NxM Matrix.
template <typename Precision, std::size_t OuterDimension,
          std::size_t InnerDimension>
class MatrixBase
    : public MatrixImpl<Precision, std::make_index_sequence<OuterDimension>,
                        InnerDimension> {
    static_assert(std::is_same_v<Precision, float> ||
                      std::is_same_v<Precision, double>,
                  "Matrix only supports single and double precision.");
    static_assert(OuterDimension * InnerDimension != 0,
                  "Both dimensions must be non-zero.");

    using base = MatrixImpl<Precision, std::make_index_sequence<OuterDimension>,
                            InnerDimension>;

   protected:
    using impl = base;

   public:
    MatrixBase() = default;
    using MatrixImpl<Precision, std::make_index_sequence<OuterDimension>,
                     InnerDimension>::MatrixImpl;

    // some useful routines for all kinds of matrices
};

// You can implement a default NxM matrix if you want.
template <typename Precision, std::size_t OuterDimension,
          std::size_t InnerDimension>
class Matrix;

// Square matrix. Reuse base class routines, and add new ones.
template <typename Precision, std::size_t Dimension>
class Matrix<Precision, Dimension, Dimension>
    : public MatrixBase<Precision, Dimension, Dimension> {
    using base = MatrixBase<Precision, Dimension, Dimension>;

   public:
    using typename base::impl;

    Matrix() = default;
    using MatrixBase<Precision, Dimension, Dimension>::MatrixBase;

    // some useful square matrix routines
};

// Can be a Nx1 Matrix specialisation here: a Vector

// 2x2 Matrix
template <typename Precision, std::size_t InnerDimension>
Matrix(enumerate<0, Precision const (&)[InnerDimension]>,
       enumerate<1, Precision const (&)[InnerDimension]>)
    -> Matrix<Precision, InnerDimension, InnerDimension>;

using Mat2f = Matrix<float, 2, 2>;

int main() { constexpr Matrix mat1{{-3.f, 5.f}, {1.f, -2.f}}; }

In fact, now you can even use constexpr matrices. That is certainly not possible with initializer_list :)

  • Related