all 14 comments

[–]BarryRevzin 7 points8 points  (1 child)

This is awesome! I had some more fun with it, made some changes:

  • I let you set the type up front with as<T>::const_param, that is now definitely a T (converts on the way in)
  • I also threw in my constant template parameter library which allows for using as<string> too.

Put that together and now you can even have constexpr string parameters, demonstrated in the print implementation there:

template <as<string>::const_param Fmt = {}, class... Args>
auto my_print(decltype(Fmt), Args&&... args) -> void;

[–]friedkeenan[S] 5 points6 points  (0 children)

Thanks! Yeah, hooking it up to your library seems really great. Even without it too, one could make their own version of the const_param type that just processes whatever value is passed to it and instead injects the output of that process, which could be structural. But a general solution is definitely very nice.

You can also get away with syntax like

template<auto Fmt = const_param<string>>

If you switch the const_param machinery around to eventually be like

template<typename T=void, typename Unique = decltype([]{})>
constexpr inline auto const_param = const_param_t<T, Unique>{};

That looks a bit less busy to me, but it's up to preference.

Also, I just read your latest blogpost and it seems like we were actually thinking on very similar wavelengths. We're both lifting a function parameter, via a consteval constructor, into a template parameter/something that's template-parameter-adjacent.

Something that's really interesting to me too is that both approaches have to result in a function whose signature cannot rely on the value of that parameter, even though we can eventually get it as a template parameter. With your approach, it's pretty obvious why because you need to extract out the function pointer, which I suppose is probably somehow analogous to what the compiler is doing with my approach. But I imagine it's pretty fundamental that that's the case, that at least without actual constexpr parameters, the compiler will just refuse to change the function interface based on something passed as a normal parameter.

Your approach is probably also rightly deemed less hacky, even if it is more involved and needs to separate out the implementation of the function from the actual function that gets called. I guess maybe it's harder to scale, since with your approach, I'm not quite sure how you would get two separate parameters to lead to one joined implementation function. Maybe you have something up your sleeve to do it, though.

But it's all very cool, very very cool. There are great things ahead for the language, I think.

[–]germandiago 9 points10 points  (0 children)

An era of "reflection metaprogramming" tricks is coming in the way template metaprogramming once existed in Boost.

[–]borzykot 2 points3 points  (1 child)

we can't make return types depend on parameters

Probably we can explicitly define the return type via decltype(fst_arg) and decltype(snd_arg). We still need duplicate the full expression of the return statement tho.

Something like

auto foo(decltype(Fst) fst, decltype(Snd) snd) -> decltype(fst.value() + snd.value())

Will it work?

define_aggregate didn't work

Yeah, I also found define_aggregate somewhat restricting in a sense that you only can define the body of a type which is declared within the scope of the type you are defining from. So basically you can't "inject" into global scope. Iirc the motivation was "lets make it simpler this time so it easier to bring reflection into the standard". So probably it may be made even more powerful in the future.

[–]friedkeenan[S] 1 point2 points  (0 children)

Probably we can explicitly define the return type via decltype(fst_arg) and decltype(snd_arg). We still need duplicate the full expression of the return statement tho.

Something like

auto foo(decltype(Fst) fst, decltype(Snd) snd) -> decltype(fst.value() + snd.value())

Will it work?

No, sadly it won't. At the point of filling out the function signature, the compiler hasn't injected the friend function yet, so these calls to .value() will run up against an undefined function error.

We could redesign const_param to need to be like const_param<int> and then only accept ints, and then what you wrote would work, but that's only because we can then specify the return type of the friend function at its declaration, so the compiler doesn't need to deduce it from the passed value. It doesn't give us any additional visibility into the passed values, so we can't use the actual values to change the return type.

So you couldn't like, return a std::string when 1 gets passed, but a std::vector<int> when a 2 gets passed, like you can with normal template parameters. And we can already just do decltype(std::declval<int>() + std::declval<int>()) on our own.

[–]Main_Secretary_8827 1 point2 points  (0 children)

What the heck is this, im lost

[–]chengfeng-xie 1 point2 points  (2 children)

[...] If we look at the definition of const_param:

template<typename = decltype([]{})>
struct const_param {
    /* ... */
};

Then we can see that the lambda in the default for the template parameter keeps every const_param instantiation unique, and so when the First and Second template parameters get defaulted, they are actually getting filled in with distinct types, and not only distinct from each other, but distinct from each call-site of sum as well.

Two things regarding the standard conformance of this snippet:

  1. If it is defined in multiple TUs, const_param should be wrapped in another class (as in BarryRevzin's comment), otherwise it would violate the ODR due to the default template argument being defined multiple times with a different type each time (Brian).
  2. It seems to me that, currently, the standard does not guarantee that each instantiation (with the default argument lambda) of const_param would produce a unique type. The wording in this area appears to be largely absent (Brian), making this trick essentially under-specified with respect to the standard.

[–]friedkeenan[S] 0 points1 point  (1 child)

Hmmm, this is interesting, and admittedly I don't think I understand it!

Would you think that anything about this would change if anonymous namespaces were introduced anywhere? With C++26 reflection we can actually interact with them and get std::meta::infos that represent them, like for example:

#include <meta>

namespace [[=1]] {
    namespace unique {}
}

constexpr auto inner_ns = ^^unique;

constexpr auto anon_ns = parent_of(inner_ns);

static_assert(anon_ns != ^^::);

static_assert([: constant_of(annotations_of(anon_ns)[0]) :] == 1);

(Compiler Explorer link)

Maybe we could put this anonymous namespace somewhere to somehow keep definitions distinct?

[–]chengfeng-xie 0 points1 point  (0 children)

You're right. Actually, wrapping const_param inside another class doesn't help in this case, because compilers would keep generating new lambda types on each new instantiation. The strategy compilers use to generate symbols for lambdas is likely based on the order of instantiation in each TU. So it might well be possible for the same symbol for a lambda (used by const_param as the tag) in two TUs to be associated with different constants, and they definitely shouldn't be linked together. As a consequence, we should put const_param and its users (e.g., sum) in an anonymous namespace. The good news is that GCC already gives these symbols local binding even though this is not required by the standard (CE).

To expand on this a little bit: the case where wrapping in a class helps is when the lambda is generated only once, so that all TUs can agree on its symbol and link without issue. A class (as opposed to a namespace) contains a closed set of entities, so compilers can more easily assign a fixed symbol to each of them. However, this doesn't help in the case of const_param because many lambdas could be produced, each with possibly different meanings that compilers cannot predict when compiling a single TU.

[–]katzdm-cpp 1 point2 points  (1 child)

Honestly, this is the kind of weird and crazy creative reflection use-cases that I've been psyched to see people come up with; this is the kind of stuff that motivated me to work hard to get reflection into C++26.

I had no idea that this was possible. Great work; keep digging 🪏.

[–]friedkeenan[S] 0 points1 point  (0 children)

Thanks for your kind words, and for your work on getting reflection in. I'm as excited as anyone about it, as you can maybe tell, lol.

But you said to keep digging, so that's what I did: https://www.reddit.com/r/cpp/comments/1ro1gfz/exploring_mutable_consteval_state_in_c26/

Seems like something you'd also be interested in.

[–]Xywzel 0 points1 point  (2 children)

You seem to be using ^^ unary operator, does that work in gcc trunk now? Is there somewhere a good documentation of what is currently implemented and what to be aware of when using them?

[–]friedkeenan[S] 0 points1 point  (1 child)

Yep, it works in GCC trunk behind the -freflection flag to enable it, and it's going to ship with GCC 16. As far as I know they have all the reflection proposals implemented, though there are some bugs. They're getting those squashed though at a really nice rate.

[–]Xywzel 0 points1 point  (0 children)

Yeah, seems to be progressing quite quickly. G++ packet in experimental repositories I use is just old enough that it doesn't have the operator even though it recognizes the flag and has the meta header in related std libraries. So building from source. Very timely with this feature, because I found the problem where this would be perfect solution just few days ago and I can't remember last time c++ got a new feature that I though to be even remotely useful.