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

2

15

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)?

Karyosome answered 17/12, 2023 at 2:2 Comment(9)
MSVC accepts your program as written. It also works when the default value of the template parameter is {}. Does clang accept std::array<int, 3> offset = std::array<int, 3>{}?Pillow
@Pillow Thanks, std::array<int, 3> offset = std::array<int, 3>{0, 0, 0} works! Array<{1, 1, 1}> arr; also needs to be Array<std::array<int, 3>{1, 1, 1}> arr;.Omidyar
This is cwg2450 and although the current grammar does not allow this, it is proposed to be allowed for the reason mentioned in the cwg.Libreville
Also note that clang trunk also starts accepting the program while gcc and msvc already accepted it from earlier versions.Libreville
"Is there an alternative and more compatible way to achieve my goals?..." Yes there is. See the solution given in the answer below. Basically, you can make it work if you explicitly write std::array before the braced init list.Libreville
No you don't NEED to define a template. I wouldn't use std::array to represent a vector. A struct position_t { int x; int y; int z; } is semantically stronger. Now you can also have struct velocity_t and you can't mix up position and velocity at compile time. Also your code becomes more readable as a result (std::array is pretty meaningless)Joijoice
Strongly related: Is it possible to pass a braced-init-list as a template argument?Comfort
Why the heck was this closed...Tagalog
@Tagalog I originally asked two different but related questions. One was already later reasked and answered elsewhere (Using Arrays as Template Parameters With Default Values in C++17 and Earlier). It's rather pointless to close this by now as both already found answers - but someone probably believed that "the rule is the rule"... If this question is about C++ lawyering, closing it would be Stack Exchange lawyering, so both activities belong to the same theme...Omidyar
L
17

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.

Libreville answered 17/12, 2023 at 3:26 Comment(6)
Related question: Is there a way to achieve something similar in C++17? (feel free to ignore if there's no quick solution, in that case I'll open a separate question).Omidyar
@Karyosome By something similar are you referring to making your program C++17 compatible or are you referring to having some other possible design pattern that can help you achieve the same thing as this program does. Or maybe both. In any case, I would recommend asking a separate question(as you also said) since this is language-lawyered and specific to why this particular program doesn't work with clang. In the new question you can also mention explicitly the requirements of your program in more detail. Like in this current question, the requirement is still somewhat unclear to me.Libreville
It’s actually CWG2049, but that’s almost the same thing.Did
@DavisHerring I've updated and mentioned CWG2049 along with [CWG2450].Libreville
Question reasked at Using Arrays as Template Parameters With Default Values in C++17 and Earlier.Omidyar
@Karyosome i see....Libreville
J
-2

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";
    }
}
Joijoice answered 17/12, 2023 at 4:48 Comment(5)
I think this answer should belong to a new question(that OP hopefully will ask) as per this comment since this parituclar question is about the error in this program with clang and language-lawyered. Though this can work as a workaround.Libreville
Fair point, I added this example to show that using a bit more (semantically correct design) these kind of boundaries of the C++ compiler can be avoided. So more of an example on how to keep things simple and stay away from compiler boundaries that needs language laywering ;)Joijoice
Yeah, this looks good design-wise.Libreville
Unfortunately this answer is not relevant to my question. The motivation is not about iterating in a list of points or coordinates. Instead, it's about creating an "array accessor" wrapper class to perform implicit index transformations using operator overleading and templates based on several compile-time parameters - which are all 3D coordinates. For example, a 3D space may have a range between (-10, -10, -10) and (10, 10, 10). With a suitable template wrapper class, I can then create an accessor with offset=(10, 10, 10) to allow its users to write array(-10, -10, -10) = 1 directly.Omidyar
Question reasked at Using Arrays as Template Parameters With Default Values in C++17 and Earlier.Omidyar

© 2022 - 2024 — McMap. All rights reserved.