Search code examples
c++templatesdeduction-guide

User-defined deduction guides for non-type template parameters


I have the beginnings of a matrix class. Here's the code-

template<int h, int w = h>
class mat {
public:
    mat() : values(h, std::vector<double>(w)) {
        if (w == h) {
            int x = 0;
            for (int y = 0; y < h; y++) {
                values[y][x] = 1;
                x++;
            }
        }
    }
    mat(std::initializer_list<std::vector<double>> matvals){
        values = matvals;
    }
    mat(int val) : values(h, std::vector<double>(w, val)) {}
    mat(const mat& m) {
        values = m.values;
    }
    mat(mat&& m) {
        values = std::move(m.values);
    }
    int width() {
        return w;
    }
    int height() {
        return h;
    }
    template<int mh, int mw = mh>
    auto operator*(const mat<mh, mw>& m) -> mat<h, mw> const {
        if (w != mh) throw std::logic_error{ "Matrices cannot be multiplied" };
        mat<h, mw> temp;
        std::vector<double> mcol(mh);
        for (int y = 0; y < mw; y++) {
            for (int mx = 0; mx < mw; mx++) {
                for (int my = 0; my < mh; my++) {
                    mcol[my] = m.values[my][mx];
                }
                temp.values[y % h][mx] = dot(values[y % h], mcol);
            }
        }
        return temp;
    }
    mat operator+(const mat& m) const {
        mat temp;
        for (int y = 0; y < h; y++) {
            for (int x = 0; x < w; x++) {
                temp.values[y][x] = values[y][x] + m.values[y][x];
            }
        }
        return temp;
    }
    std::vector<double>& operator[](int y) {
        return values[y];
    }
    mat& operator=(const mat& m) {
        values = m.values;
        return *this;
    }
    mat& operator=(mat&& m) {
        values = std::move(m.values);
        return *this;
    }
private:
    std::vector<std::vector<double>> values;

    template<int mw, int mh>
    friend class mat;
};

Here's how this class is used so far-

mat<2, 4> mat1 = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
};

mat<4, 3> mat2 = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9},
    {10, 11, 12}
};

auto mat3 = mat1 * mat2;

Notice the redundancy? If a user wants to create a matrix with the std::initializer_list constructor, then they have to first specify the width and height within the template parameters. Also, there's the issue of if they use a std::initializer_list whose dimensions are different from what was specified within the template arguments, then the behavior would be undefined. How do I write a deduction guide for non-type template parameters? I know how to do it with a basic template, but everything I try doing like you would normally gives many compiler errors. Here's desired behavior-

mat mat1 = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
};

mat mat2 = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9},
    {10, 11, 12}
};

auto mat3 = mat1 * mat2;

EDIT: Anyone that actaully wants to make a matrix class should not make the width and height template parameters. It just unnecesarily compicates things.
EDIT 2: This is a ridiculous example either way. I made the width and height template parameters but used std::vector to store the values.


Solution

  • if you provide a constructor like this:

    template<int w, int h>
    struct A{
        A(double (&&c)[w][h]); // `double const (&c)[w][h]` is also OK.
    };
    

    without any other deduction guide, you can use it as follow:

    A a{{
        {1, 2, 3},
        {4, 5, 6}
    }};
    A b = {{
        {1, 2, 3},
        {4, 5, 6}
    }};
    A c({
        {1, 2, 3},
        {4, 5, 6}
    });
    

    but we may think the outer braces are awful, so we have to provide a special constructor and deduction guide:

    namespace Impl{
        template<typename T, typename = void>
        struct helper;
        template<int h1, int... hs>
        struct helper<std::integer_sequence<int, h1, hs...>, std::enable_if_t<((h1 == hs) && ...)>>{
            static constexpr int w = sizeof...(hs) + 1;
            static constexpr int h = h1;
        };
        template<int... hs>
        inline constexpr int helper_w = helper<std::integer_sequence<int, hs...>>::w;
        template<int... hs>
        inline constexpr int helper_h = helper<std::integer_sequence<int, hs...>>::h;
    }
    
    template<int w, int h>
    struct A{
        template<int... hs, typename = std::enable_if_t<w + 1 == Impl::helper_w<h, hs...>>>
        A(double (&&... head)[hs]);
    };
    
    template<int... hs>
    A(double (&&... head)[hs]) -> A<Impl::helper_w<hs...>, Impl::helper_h<hs...>>;
    

    and then you can use it as you want:

    A a{
        {1, 2, 3},
        {4, 5, 6}
    };
    A b = {
        {1, 2, 3},
        {4, 5, 6}
    };