Can you use a braced-init-list as a (default) template argument?

687 Views Asked by At

I need to define a C++ template that accepts several 3D coordinates as their parameters. When all dimensions of these coordinates are defined as separate integer variables, the parameter list would become exceedingly long - 3 coordinates need 9 parameters, which makes the template hard to use.

Thus, it's highly desirable to declare the templates in a way to use compile-time arrays. Their default arguments should also be declared directly at the location of the template declaration as values, rather than as variable names.

After some experimentation, to my surprise, I found GCC 13 will accept the following C++ program with std=c++20:

#include <cstdio>
#include <array>

template <
    std::array<int, 3> offset = {0, 0, 0}
>
struct Array
{
    void operator() (int i, int j, int k) 
    {
        printf("(%d, %d, %d)\n", i + offset[0], j + offset[1], k + offset[2]);
    }
};

int main(void)
{
    Array arr_default;
    arr_default(0, 0, 0);

    Array<{1, 1, 1}> arr;
    arr(0, 0, 0);

    return 0;
}

However, clang 18 rejects the braced-init-list as invalid:

test2.cpp:5:30: error: expected expression
    5 |         std::array<int, 3> offset = {0, 0, 0}
      |                                     ^
test2.cpp:17:8: error: no viable constructor or deduction guide for deduction of template arguments of 'Array'
   17 |         Array arr_default;
      |               ^
test2.cpp:7:8: note: candidate template ignored: couldn't infer template argument 'offset'
    7 | struct Array
      |        ^
test2.cpp:7:8: note: candidate function template not viable: requires 1 argument, but 0 were provided
    7 | struct Array
      |        ^~~~~
test2.cpp:20:8: error: expected expression
   20 |         Array<{1, 1, 1}> arr;
      |               ^
3 errors generated.

Question

Is it really a legal C++ program? If it is, what syntax should I use to convince clang to accept it? If it's not, how can I fix the code (and should I report a GCC bug for accepting it unquestionably)?

2

There are 2 best solutions below

6
On BEST ANSWER

Problem

This is CWG 2450 and/or CWG 2049 and although the current grammar does not allow this, it is proposed to be allowed/valid for the reason mentioned below. That means, Gcc is just preemptively allowing the syntax. From CWG 2450:

Since non-type template parameters can now have class types, it would seem to make sense to allow a braced-init-list as a template-argument, but the grammar does not permit it.

(emphasis mine)

In case you're wondering how the current grammar makes does not allow this, from temp.name#1:

template-argument:
    constant-expression
    type-id
    id-expression 

And since {1, 1, 1} is not any of the above three listed constructs, it cannot be used as a template argument as per the current grammar rules.


Is there an alternative and more compatible way to achieve my goals?

Solution

You can explicitly write std::array before the braced init list as shown below:

#include <cstdio>
#include <array>

template <
//------------------------------vvvvvvvvvv----------->added this
    std::array<int, 3> offset = std::array{0, 0, 0}
>
struct Array
{
    void operator() (int i, int j, int k) 
    {
        printf("(%d, %d, %d)\n", i + offset[0], j + offset[1], k + offset[2]);
    }
};

int main(void)
{
    Array arr_default;
    arr_default(0, 0, 0);
//--------vvvvvvvvvv------------------->added this
    Array<std::array{1, 1, 1}> arr;
    arr(0, 0, 0);

    return 0;
}

Note

Also note that clang trunk also starts accepting the program while gcc and msvc already accepted it from earlier versions.

5
On

In current C++ I would expect your code to look something like this:

#include <iostream>
#include <format>
#include <vector>

// using namespace std; <== unlearn to use this (risk of nameclashes in big projects)

// this is how you declare a structure/class template
template<typename type_t> 
struct vector_3d_t
{
    type_t x;
    type_t y;
    type_t z;
};

// make new strong types (don't use `using` for this)
struct position_3d_t : public vector_3d_t<int> {};
struct velocity_3d_t : public vector_3d_t<int> {};

position_3d_t update(const position_3d_t& position, const velocity_3d_t& velocity)
{
    return position_3d_t 
    { 
        position.x + velocity.x,
        position.y + velocity.y,
        position.z + velocity.z 
    };
}

// Overload operator<< so ostreams (like std::cout) know
// how to format one vector_3d_t
template <typename type_t>
std::ostream& operator<<(std::ostream& os, const vector_3d_t<type_t>& vector)
{
    os << std::format("[{},{},{}]", vector.x, vector.y, vector.x );
    return os;
}


int main()
{
    // C++'s vector which is dynamically resizable array
    // and this is how you can initialize multiple positios 
    // in a few lines of code. 
    std::vector<position_3d_t> positions
    {{
        {1,1,1}, 
        {2,2,2}, 
        {3,3,3}
    }};

    // learn about range base for loops 
    // and use them when you don't need indices
    for(const auto& position : positions)
    {
        std::cout << position << "\n";
    }
}