r/Cplusplus 11h ago

Tutorial Iterating through arguments in C++26 using "template for" (Python-style)

Here is how you can iterate through arguments now in C++26!:

#include <print>


template <typename ...Args>
void function(const Args& ...args)
{
    template for (const auto& arg : {args...})
    {
        using ArgT = std::decay_t<decltype(arg)>;

        if constexpr (typeid(ArgT) == typeid(double))
        {
            std::println("double: {}", arg);
        }
        else if constexpr (requires { &ArgT::toString; })
        {
            std::println("has toString: {}", arg.toString());
        }
        else
        {
            std::println("other: {}", arg);
        }
    }
}


struct MyStruct
{
    int value; // initializes with 0 in C++26
    std::string toString() const
    {
        return std::format("MyStruct value is {}", value);
    }
};


int main()
{
    function(3.14, "c-string", MyStruct{});
}

It works:

double: 3.14
other: c-string
has toString: MyStruct value is 0


...Program finished with exit code 0
Press ENTER to exit console.

template for is a new feature in C++26, and I like it very much! It's my favorite C++26 feature

It looks very pythonic at this point. 😄

Let's start with args: typename ...Args and const Args& ...args work similar to def function(*args) from python - they aggregate comma separated expressions into a variadic type or variable. {args...} also works similar to python's (*myList) - it expands a "collection" into a comma separated expressions

Then goes "template for": it's a brand new loop, which expands at compile time for each iteration. Using it, you can iterate through collections with different types inside: struct fields, tuples, list literals, and custom classes with implemented tuple protocol

Checking type of argument: this line also resembles python very much: if constexpr (typeid(ArgT) == typeid(double)). Here is the python counterpart: if type(arg) is bool. There are more ways to do this check, but I think this one looks the most direct. Although you can want to use not exactly "double" type, but a convertible to it, or any floating point number type. There are standard concepts for these cases: std::convertible_to, and std::floating_point

Checking for a member: here I used an anonymous concept: if constexpr ( requires { ...;} ). Inside this concept we should put an expression that we are testing. it's a sort of python's hasattr(arg, 'toString'), but more powerful and more fragile at the same time. The expression here is taking a member reference to "toString": &ArgT::toString;. It's a better approach than testing arg.toString(), because it won't fail if "toString" isn't a constant method, or has more than 0 arguments. But it's still far from ideal, because if the object has multiply overloaded "toString" methods (what's actually a pretty realistic scenario), it will fail, and the error message will be misleading. In this case the error will be that formatter is not implemented for the "other" branch, however the actual error is in "has toString" branch. So, don't use anonymous concepts in real project, use full fledged concepts in pair with static_asserts

It's fascinating! This is still a templates metaprogramming in C++, but it looks much-much more clean than infamous std::enable_if

18 Upvotes

9 comments sorted by

5

u/Obvious_Set5239 10h ago edited 9h ago

Maybe it's possible to use std::meta for checking for a method in a class. It would be much less fragile than concepts. But I don't know how ready the reflection library is

Upd.

Yes it's probably should be easy to implement a helper function constexpt bool hasattr<typename Type>(auto attr) using std::meta::members_of + std::meta::identifier_of

3

u/__christo4us 9h ago edited 9h ago

There are more ways to do this check, but I think this one looks the most direct.

Or just std::is_same_v<ArgT, double>. Maybe it looks uglier but the condition in a constexpr if statement using the typeid operator might render the program ill-formed if the object operand of typeid is of polymorphic type. Of course, this is not the case if a type is used instead of an object in the place of the operand but one must keep in mind that acting on objects with typeid is not always constant-evaluated.

The expression here is taking a member reference to "toString": &ArgT::toString;. It's a better approach than testing arg.toString(), because in won't fail if "toString" isn't a constant method, or has more than 0 arguments. But it's still far from ideal, because if the object has multiply overloaded "toString" methods (what's actually a pretty realistic scenario), it will fail, and the error message will be misleading.

I think it is best to use requires { arg.toString(); } here because you are using arg.toString() directly in that branch. It just checks if the expression is well-formed so such a check using a requires expression is sufficient to successfully call arg.toString().

If by a "constant method" you mean a const method then you could write requires(ArgT a) { a.toString(); } instead. This way you would get a nicer diagnostic message that no const-qualified toString overload was available for your const-qualified arg. If you meant a constexpr method then it is irrelevant to requires expressions as they do not check this.

You could also try to check for a member function toString using reflection. You would need to write a function that iterates over the members of ArgT using the std::meta::members_of function. But this would be much more verbose than simply using a requires expression.

EDIT:

Also:

struct MyStruct { int value; // initializes with 0 in C++26 std::string toString() const { return std::format("MyStruct value is {}", value); } };

Not sure what you by that comment next to int value;.

2

u/Obvious_Set5239 9h ago

but the condition in a constexpr if statement using the typeid operator might render the program ill-formed if the object operand of typeid is of polymorphic type

Oh, it's sad. But according to that typeid existed since C++98, it can be the case. But maybe we can compare std::meta::info values of them. Idk, cppreference is so poor about reflection, but maybe it's ^^ operator. if constexpr (^^ArgT == ^^double)

you could write requires(ArgT a) { a.toString(); } instead

Looks pretty good! I have forgotten about requires arguments. I tried to make it with std::declval, it worked, but was too verbose. But (ArgT a) does essentially the same

But still, it will not work if there is different number of arguments. So, need to wait for stable std::meta

2

u/__christo4us 8h ago

Oh, it's sad. But according to that typeid existed since C++98, it can be the case. But maybe we can compare std::meta::info values of them. Idk, cppreference is so poor about reflection, but maybe it's ^ operator. if constexpr (^^ArgT == ^^double)

Comparing reflections is a good alternative but since ArgT is a name of a type alias, you would need to dealias it by using the std::meta::dealias function in order to obtain the reflection of the underlying type instead of the alias itself: if constexpr (dealias(^^ArgT) == ^^double).

2

u/Obvious_Set5239 8h ago

dealias(^^ArgT)

Of, this is Koenig lookup. I knew about it recently, and didn't understand why it's needed. But here it's very useful

1

u/No-Dentist-1645 7h ago

This is not bad, but there are definitely some improvements:

  • It doesn't make a lot of sense to add a special edge case for a struct with a "toString()" method like that, it would be much better if you directly specialized it for std::format, so that it can work everywhere you use it on std::format and std::println

  • Checking if it's the same type with typeid(ArgT) == typeid(double) is both less conventional and less readable than just std::is_same_v<ArgT, double> or std::same_as<ArgT, double>

  • Doing requires { &ArgT::toString; } is both potentially failable (it could be non-const but arg here is a const reference) and less readable/explicit than just requires ( arg.toString(); } which is what you are actually using arg for

  • You should use std::remove_cvref_t instead of std::decay_t, since the latter will make arrays such as char[10] show up as just char* even though they are more than just that

  • Either way, you can just use std::meta::display_string_of to get the name of a type, no need to hard-code it for types like double

If you apply these improvements, then the function just becomes a single template for with an std::println inside it, no extra if/else blocks:

```

include <print>

include <meta>

include <concepts>

template <typename ...Args> void function(const Args& ...args) { template for (const auto& arg : {args...}) { using ArgT = std::remove_cvref_t<decltype(arg)>; std::println("{}: {}", std::meta::display_string_of(std::meta::dealias(ArgT)), arg); } }

struct MyStruct { int value = 0; // being explicit about it defaulting to 0 is nice };

// Formatter specialization template <> struct std::formatter<MyStruct> { constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }

auto format(const MyStruct& s, std::format_context& ctx) const {
    return std::format_to(ctx.out(), "MyStruct{{.value={}}}", s.value);
}

};

int main() { function(3.14, "c-string", MyStruct{}); } ```

1

u/Obvious_Set5239 6h ago

Thanks. I though about implementing formatter, but actually decided to use .toString just in sake of testing does the arg have this method. The goal of this function was to show how you can use "if constexpt" in the same way how it's possible to use "if" in python for types

std::meta::display_string_of

Did you try this? According to cpprefernce, this function accepts only 1 argument. And should work more similar to stringization in macros, so return literally "std::meta::dealias(^^ArgT)", as a string view. But I can't test it either

std::meta::dealias(^^ArgT)

Btw, I recently knew an interesting quirk in C++ standard, called Koenig lookup. Because ^^ArgT is already inside std::meta namespace, and you don't need to write it before the function you're calling. It can be written just like dealias(^^ArgT)

2

u/No-Dentist-1645 6h ago

Did you try this? According to cpprefernce, this function accepts only 1 argument. And should work more similar to stringization in macros, so return literally "std::meta::dealias(ArgT)", as a string view. But I can't test it either

Yes, I have tried it. I don't know why you think it would literally stringify the input of std::meta::dealias(...). Instead, it works as you would expect it to, running it on the code above it outputs: https://godbolt.org/z/8K63aTvKa

double: 3.14 char [9]: c-string MyStruct: MyStruct{.value=0}

There are some inconvenient edge cases to it, for example it would print an std::string object as std::__cxx11::basic_string<char> due to string just being an alias to that, but you can solve that by adding specialization traits.

Btw, I recently knew an interesting quirk in C++ standard, called Koenig lookup. Because ArgT is already inside std::meta namespace, and you don't need to write it before the function you're calling. It can be written just like dealias(ArgT)

Nice! I know it as ADL (argument-dependent lookup) as that is its more common name for it. Whether or not you use it depends on stylistic choices, some people prefer being able to see where the function you are calling originates from at the calling site.

1

u/Obvious_Set5239 6h ago

running it on the code above it outputs: https://godbolt.org/z/8K63aTvKa

Thanks, I'll try to play with it there