Metaprograming: Failure of Function Definition Defines a Separate Function
Asked Answered
U

8

30

In this answer I define a template based on the type's is_arithmetic property:

template<typename T> enable_if_t<is_arithmetic<T>::value, string> stringify(T t){
    return to_string(t);
}
template<typename T> enable_if_t<!is_arithmetic<T>::value, string> stringify(T t){
    return static_cast<ostringstream&>(ostringstream() << t).str();
}

dyp suggests that rather than the is_arithmetic property of the type, that whether to_string is defined for the type be the template selection criteria. This is clearly desirable, but I don't know a way to say:

If std::to_string is not defined then use the ostringstream overload.

Declaring the to_string criteria is simple:

template<typename T> decltype(to_string(T{})) stringify(T t){
    return to_string(t);
}

It's the opposite of that criteria that I can't figure out how to construct. This obviously doesn't work, but hopefully it conveys what I'm trying to construct:

template<typename T> enable_if_t<!decltype(to_string(T{})::value, string> (T t){
    return static_cast<ostringstream&>(ostringstream() << t).str();
}
Urolith answered 12/5, 2015 at 11:45 Comment(0)
F
15

Freshly voted into the library fundamentals TS at last week's committee meeting:

template<class T>
using to_string_t = decltype(std::to_string(std::declval<T>()));

template<class T>
using has_to_string = std::experimental::is_detected<to_string_t, T>;

Then tag dispatch and/or SFINAE on has_to_string to your heart's content.

You can consult the current working draft of the TS on how is_detected and friends can be implemented. It's rather similar to can_apply in @Yakk's answer.

Fluvial answered 12/5, 2015 at 17:24 Comment(14)
Is @Yakk on the committee?Mcglothlin
@Mcglothlin You'll have to ask him :)Fluvial
@Fluvial Does this mean that I could just define my ostringstream overload like: template<typename T> enable_if_t<!experimental::is_detected<decltype(std::to_string(std::declval<T>())), T>::value, string> (T t){ return static_cast<ostringstream&>(ostringstream() << t).str(); }Urolith
@JonathanMee No, is_detected's first parameter is a template template parameter. You need an alias template.Fluvial
@Fluvial Can you help me understand what the alias provides that calling decltype inline does not?Urolith
@JonathanMee Clarity. Without the additional alias, has_to_string would look like a mess. No-one wants to mentally parse template<class T> using has_to_string = std::experimental::is_detected<decltype(std::to_string(std::declval<T>())), T>;Irradiation
@JonathanMee An alias template is a template and so can be passed as an argument to a template template parameter.Fluvial
@Fluvial Conversely you're saying that decltype cannot be passed as a template argument? Seems like I've done that before...Urolith
@JonathanMee It's a template template parameter (hint, that repetition was intentional), and it expects a class or alias template as the argument.Fluvial
@Fluvial Ugh, that's a bit of a hassle to have to define an alias template for anything I want to pass to is_detected, particularly since I already know the type in the calling template. But now that I think about it, if it wasn't a template template parameter it would be evaluated inline, rather than being evaluated by is_detected.Urolith
I have chosen to accept this answer because for future generations this will be the cleanest solution. There are so many good answers here though. As commented in this answer Yakk's answer has the capability to achieve what is_detected will standardize. I'd also like to point out that for the case where only validating to_string will ever be required Barry's answer has a simplicity to it that is appealing.Urolith
@Fluvial I've updated my answer to the question that originally prompted this question, I'd appreciate if you could look over it and confirm I'm "doing it right".Urolith
I wonder if it's worth trying to propose like template <class T> using has_to_string = std::is_valid(std::to_string(std::declval<T>())). Where std::is_valid is magic. is_detected is great, but you still need that extra named type alias somewhere.Mcglothlin
Sadly is_detected still didn't make it into Visual Studio 2019. I added a sample cross platform example of this here.Urolith
M
18

Using Walter Brown's void_t:

template <typename...>
using void_t = void;

It's very easy to make such a type trait:

template<typename T, typename = void>
struct has_to_string
: std::false_type { };

template<typename T>
struct has_to_string<T, 
    void_t<decltype(std::to_string(std::declval<T>()))>
    > 
: std::true_type { };
Mcglothlin answered 12/5, 2015 at 12:24 Comment(16)
Very elegant, +1. Do you happen to know if that has a good chance of making standardisation?Irradiation
@Irradiation No clue. At least it's very easy to implement yourself :)Mcglothlin
@Irradiation It's in the draft of the next standard. GCC and Clang have it when compiling with -std=c++1z (and they have __void_t with -std=c++11) and MSVC 2015 has it.Viniferous
@Viniferous Seems like an answer working with the standardized tool rather than writing our own. Could we either edit this answer or write a new one describing that?Urolith
@Viniferous __void_t yes, not void_tMcglothlin
@Mcglothlin What do you mean?Viniferous
@Viniferous I don't see either Clang 3.6.0 or gcc 5.1.0 having std::void_t. Both have std::__void_t though.Mcglothlin
@Viniferous Oh, that's the only change? we use this answer's code but we write using __void_t = void? Or are you saying that is already defined?Urolith
@Mcglothlin I don't know what to say. I saw it in their <type_traits> here and here.Viniferous
@JonathanMee Change what? I don't know what you're talking about. Everything you need is in Barry's answer. You can replace void_t with std::void_t or std::__void_t if your standard library implements it, the result will be the same.Viniferous
@Viniferous So I'd just use this code in an enable_if_t, like this: template<typename T> enable_if_t<has_to_string<T>::value, string> stringify(T t){ return to_string(t); } template<typename T> enable_if_t<!has_to_string<T>::value, string> stringify(T t){ return static_cast<ostringstream&>(ostringstream() << t).str(); } Hmmm... I must be misunderstanding how __void_t works, can you elaborate a bit?Urolith
@JonathanMee: If the compiler can figure out what the type of std::to_string(std::declval<T>()) is, then it passes that type to __void_t, which is simply void, and the true specialization is used. If the compiler can't figure out what std::to_string(std::declval<T>()) is, it abandons that specialization altogether, and selects the only remaining option: the false. __void_t does nothing but discard the return type.Monogamous
@MooingDuck So, Matthis Vega's answer doesn't use __void_t at all. And seems to do the exact same thing. What I'm trying to figure out is what's the benefit of using __void_t?Urolith
@JonathanMee The benefit of using void_t is that it works. Matthis Vega's answer doesn't work.Mcglothlin
@Viniferous Sorry I'm taking forever to accept an answer I'm struggling to understand the code in some of these answers. For example I still think I don't understand the point of void_t rather than have you keep explaining it to me in comments I've asked the question here: https://mcmap.net/q/472561/-understanding-alias-templates/2642059Urolith
@JonathanMee There's no time limit for accepting an answer, and no obligation to do so ever.Mcglothlin
P
18

First, I think SFINAE should usually be hidden from interfaces. It makes the interface messy. Put the SFINAE away from the surface, and use tag dispatching to pick an overload.

Second, I even hide SFINAE from the traits class. Writing "can I do X" code is common enough in my experience that I don't want to have to write messy SFINAE code to do it. So instead I write a generic can_apply trait, and have a trait that SFINAE fails if passed the wrong types using decltype.

We then feed the SFIANE failing decltype trait to can_apply, and get out a true/false type depending on if the application fails.

This reduces the work per "can I do X" trait to a minimal amount, and places the somewhat tricky and fragile SFINAE code away from day-to-day work.

I use C++1z's void_t. Implementing it yourself is easy (at the bottom of this answer).

A metafunction similar to can_apply is being proposed for standardization in C++1z, but it isn't as stable as void_t is, so I'm not using it.

First, a details namespace to hide the implementation of can_apply from being found by accident:

namespace details {
  template<template<class...>class Z, class, class...>
  struct can_apply:std::false_type{};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:
    std::true_type{};
}

We can then write can_apply in terms of details::can_apply, and it has a nicer interface (it doesn't require the extra void being passed):

template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z, void, Ts...>;

The above is generic helper metaprogramming code. Once we have it in place, we can write a can_to_string traits class very cleanly:

template<class T>
using to_string_t = decltype( std::to_string( std::declval<T>() ) );

template<class T>
using can_to_string = can_apply< to_string_t, T >;

and we have a trait can_to_string<T> that is true iff we can to_string a T.

The work require to write a new trait like that is now 2-4 lines of simple code -- just make a decltype using alias, and then do a can_apply test on it.

Once we have that, we use tag dispatching to the proper implementation:

template<typename T>
std::string stringify(T t, std::true_type /*can to string*/){
  return std::to_string(t);
}
template<typename T>
std::string stringify(T t, std::false_type /*cannot to string*/){
  return static_cast<ostringstream&>(ostringstream() << t).str();
}
template<typename T>
std::string stringify(T t){
  return stringify(t, can_to_string<T>{});
}

All of the ugly code is hiding in the details namespace.

If you need a void_t, use this:

template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;

which works in most major C++11 compilers.

Note that the simpler template<class...>using void_t=void; fails to work in some older C++11 compilers (there was an ambiguity in the standard).

Putandtake answered 12/5, 2015 at 15:43 Comment(13)
What I don't like about this form of tag dispatch is that the meaning of true_type and false_type is defined only by the caller. It is not immediately obvious what is tested from just the set of dispatch targets. The MF(Args) form of concepts emulation has the advantage that it is lazily evaluated, as opposed to alias templates. This can be used to prevent substitution failures which don't appear in the immediate context.Tycoon
@Tycoon true. Which is why I usually give it a commented-out name. Added ` /*can to string*/` in both cases. I'm uncertain what kind of errors your MF approach avoids in the non-immediate context that mine doesn't? Is it because of the choice<> overloads or something?Putandtake
No, and I'm not sure if you'd ever encounter them in tag dispatch (as opposed to concepts emulation). If there's a complex expression like is_complete<T>::value && is_trivial<T>::value, then you'll have to split this up and make sure that is_trivial<T> is not instantiated if is_complete is false (otherwise UB). For more complex traits, is_trivial can be an alias template, so you'd have to delay instantiation. OTOH, is_trivial(T) does not immediately instantiate is_trivial.Tycoon
@Yakk Are you on the library fundamentals group? std::experimental::is_detected is your can_apply, pretty much.Mcglothlin
@Mcglothlin nope. Someone else has noticed the similarity and shown me a link to the paper before, however. It was just me getting tired as writing the void_t SFINAE template boilerplate again and again.Putandtake
I was wondering: Is it possible to avoid a long and/or complicated expression to get a variable of type T and the proper value category inside the SFINAE expression? The best I could come up with is to pass an instantiation of template<class T> struct my_declval{ T&& operator()(); }; instead of T to to_string_t, and then use T{}(), but that's still ugly.Tycoon
@Tycoon template<class T>T&& a(); makes it a<T>(), only 1 character longer than yours? Seems rude, as there are only 53 1 character identifiers.Putandtake
Not just that, but it also has ugly template brackets :( I was thinking about variable templates, that is if expr<T> is possible (I guess so, using template<class T> extern T&& expr;) - but I'd much prefer a solution not using any template brackets..Tycoon
Sorry I'm taking forever to accept an answer I'm struggling to understand the code in some of these answers. For example I still think I don't understand the point of void_t I've asked about it here, and since you have a good understanding of it perhaps you'd care to weigh in?Urolith
The point? It takes as input a type, and discards it. I need a void there for template specialization pattern matching, but I only want that pattern to match if a particular expression can be evaluated. Sure I could std::conditional_t< true, void, Z<Ts...> > instead, but that makes the point where I'm doing the pattern matching more noisy. Once you understand void_t as a type function * -> void, using conditional which is roughly bool, t1, t2 -> t1|t2 makes as much sense as preceding every statement in a C++ program with if (true).Putandtake
@Yakk OK I think I have void_t under the belt. Next question, can you help me understand what template<template<class...>class Z, class, class...> means. I just don't understand what Z is in this case, and template arguments with no variable? That does that accomplish?Urolith
@jonathan Z is a template argument that is a template itself. I did not need to name it either. I only needed the names in the specialization (below), because I only used the names there. Specializations are pattern matches on arguments passed to primary template, so the primary template needs to take stuff that the specialization will pattern match, even if it does not use it.Putandtake
"I think SFINAE should usually be hidden from interfaces". Disagree. Type requirements should always be a part of the interface.Michaud
F
15

Freshly voted into the library fundamentals TS at last week's committee meeting:

template<class T>
using to_string_t = decltype(std::to_string(std::declval<T>()));

template<class T>
using has_to_string = std::experimental::is_detected<to_string_t, T>;

Then tag dispatch and/or SFINAE on has_to_string to your heart's content.

You can consult the current working draft of the TS on how is_detected and friends can be implemented. It's rather similar to can_apply in @Yakk's answer.

Fluvial answered 12/5, 2015 at 17:24 Comment(14)
Is @Yakk on the committee?Mcglothlin
@Mcglothlin You'll have to ask him :)Fluvial
@Fluvial Does this mean that I could just define my ostringstream overload like: template<typename T> enable_if_t<!experimental::is_detected<decltype(std::to_string(std::declval<T>())), T>::value, string> (T t){ return static_cast<ostringstream&>(ostringstream() << t).str(); }Urolith
@JonathanMee No, is_detected's first parameter is a template template parameter. You need an alias template.Fluvial
@Fluvial Can you help me understand what the alias provides that calling decltype inline does not?Urolith
@JonathanMee Clarity. Without the additional alias, has_to_string would look like a mess. No-one wants to mentally parse template<class T> using has_to_string = std::experimental::is_detected<decltype(std::to_string(std::declval<T>())), T>;Irradiation
@JonathanMee An alias template is a template and so can be passed as an argument to a template template parameter.Fluvial
@Fluvial Conversely you're saying that decltype cannot be passed as a template argument? Seems like I've done that before...Urolith
@JonathanMee It's a template template parameter (hint, that repetition was intentional), and it expects a class or alias template as the argument.Fluvial
@Fluvial Ugh, that's a bit of a hassle to have to define an alias template for anything I want to pass to is_detected, particularly since I already know the type in the calling template. But now that I think about it, if it wasn't a template template parameter it would be evaluated inline, rather than being evaluated by is_detected.Urolith
I have chosen to accept this answer because for future generations this will be the cleanest solution. There are so many good answers here though. As commented in this answer Yakk's answer has the capability to achieve what is_detected will standardize. I'd also like to point out that for the case where only validating to_string will ever be required Barry's answer has a simplicity to it that is appealing.Urolith
@Fluvial I've updated my answer to the question that originally prompted this question, I'd appreciate if you could look over it and confirm I'm "doing it right".Urolith
I wonder if it's worth trying to propose like template <class T> using has_to_string = std::is_valid(std::to_string(std::declval<T>())). Where std::is_valid is magic. is_detected is great, but you still need that extra named type alias somewhere.Mcglothlin
Sadly is_detected still didn't make it into Visual Studio 2019. I added a sample cross platform example of this here.Urolith
I
10

You could write a helper trait for this using expression SFINAE:

namespace detail
{
    //base case, to_string is invalid
    template <typename T>
    auto has_to_string_helper (...) //... to disambiguate call
       -> false_type;

    //true case, to_string valid for T
    template <typename T>
    auto has_to_string_helper (int) //int to disambiguate call
       -> decltype(std::to_string(std::declval<T>()), true_type{});
}

//alias to make it nice to use
template <typename T>
using has_to_string = decltype(detail::has_to_string_helper<T>(0));

Then use std::enable_if_t<has_to_string<T>::value>

Demo

Irradiation answered 12/5, 2015 at 12:3 Comment(6)
This appears to work well, thanks for the rewrite. It looks like has_to_string_helper is a variable template or something, but what is the -> doing? I'm not familiar with that use of the arrow operator.Urolith
has_to_string_helper is a template function declaration. We never need to define it as we are just using it as a compile-time construct. The -> is using C++11's trailing return type syntax. I think it's a bit clearer in this case.Irradiation
Wouldn't I have to write an overload of has_to_string_helper for every type that to_string actually accepts for this to correctly return true_type? That seems like a lot of work!Urolith
No, it's templated. The true case will be used if that decltype expression is valid, otherwise the false case will be used.Irradiation
Ah, so the int is just cause you're using functions instead of objects to hold your types, so you need an overload.Urolith
The int is there because the false_type overload will still be valid, so we need something to disambiguate the call. Here is a really good article which gives a much nicer way to do this.Irradiation
T
4

I think there are two problems: 1) Find all viable algorithms for a given type. 2) Select the best one.

We can, for example, manually specify an order for a set of overloaded algorithms:

namespace detail
{
    template<typename T, REQUIRES(helper::has_to_string(T))>
    std::string stringify(choice<0>, T&& t)
    {
        using std::to_string;
        return to_string(std::forward<T>(t));
    }
    
    template<std::size_t N>
    std::string stringify(choice<1>, char const(&arr)[N])
    {
        return std::string(arr, N);
    }
    
    template<typename T, REQUIRES(helper::has_output_operator(T))>
    std::string stringify(choice<2>, T&& t)
    {
        std::ostringstream o;
        o << std::forward<T>(t);
        return std::move(o).str();
    }
}

The first function parameter specifies the order between those algorithms ("first choice", "second choice", ..). In order to select an algorithm, we simply dispatch to the best viable match:

template<typename T>
auto stringify(T&& t)
    -> decltype( detail::stringify(choice<0>{}, std::forward<T>(t)) )
{
    return detail::stringify(choice<0>{}, std::forward<T>(t));
}

How is this implemented? We steal a bit from Xeo @ Flaming Dangerzone and Paul @ void_t "can implement concepts"? (using simplified implementations):

constexpr static std::size_t choice_max = 10;
template<std::size_t N> struct choice : choice<N+1>
{
    static_assert(N < choice_max, "");
};
template<> struct choice<choice_max> {};


#include <type_traits>

template<typename T, typename = void> struct models : std::false_type {};
template<typename MF, typename... Args>
struct models<MF(Args...),
                decltype(MF{}.requires_(std::declval<Args>()...),
                         void())>
    : std::true_type {};

#define REQUIRES(...) std::enable_if_t<models<__VA_ARGS__>::value>* = nullptr

The choice classes inherit from worse choices: choice<0> inherits from choice<1>. Therefore, for an argument of type choice<0>, a function parameter of type choice<0> is a better match than choice<1>, which is a better match than choice<2> and so on [over.ics.rank]p4.4

Note that the more specialized tie breaker applies only if neither of two functions is better. Due to the total order of choices, we'll never get into that situation. This prevents calls from being ambiguous, even if multiple algorithms are viable.

We define our type traits:

#include <string>
#include <sstream>
namespace helper
{
    using std::to_string;
    struct has_to_string
    {
        template<typename T>
        auto requires_(T&& t) -> decltype( to_string(std::forward<T>(t)) );
    };
    
    struct has_output_operator
    {
        std::ostream& ostream();
        
        template<typename T>
        auto requires_(T&& t) -> decltype(ostream() << std::forward<T>(t));
    };
}

Macros can be avoided by using an idea from R. Martinho Fernandes:

template<typename T>
using requires = std::enable_if_t<models<T>::value, int>;

// exemplary application:

template<typename T, requires<helper::has_to_string(T)> = 0>
std::string stringify(choice<0>, T&& t)
{
    using std::to_string;
    return to_string(std::forward<T>(t));
}
Tycoon answered 12/5, 2015 at 12:56 Comment(13)
Shouldn't (ostringstream{} << x).str() fail since ostream doesn't have an str() method?Paynter
@0x499602D2 Yes, it will fail. It needs to be static_cast<ostringstream&>(ostringstream{} << x).str(): https://mcmap.net/q/472562/-use-an-anonymous-stringstream-to-construct-a-string/2642059Urolith
@0x499602D2 Hmmm. I've tested this in fact, and it did work. Not sure why, though. I'll try to find out.Tycoon
@Tycoon I think it might be an extension. It certainly isn't supposed to work.Paynter
@0x499602D2 Ah yes, it certainly seems so: libc++ defines the following overload: inline _LIBCPP_INLINE_VISIBILITY typename enable_if<!is_lvalue_reference<_Stream>::value && is_base_of<ios_base, _Stream>::value, _Stream&&>::type operator<<(_Stream&& __os, const _Tp& __x)Tycoon
@0x499602D2 If you're using gcc prior to 5.0 there is a known bug with the move constructor: gcc.gnu.org/bugzilla/show_bug.cgi?id=54316 You could get around this by declaring and inserting into the ostringstream then calling ostringstream::str() inline.Urolith
@Tycoon In Xeo's post, what's with all the ...s in, e.g., template<unsigned N, EnableIf<is_multiple_of<N, 15>>...>Mcglothlin
@Mcglothlin It's for stopping the user from having to provide an extra template parameter. See this.Irradiation
@Tycoon I think I understand everything in this post but this line: struct models<MF(Args...), decltype(MF{}.requires_(std::declval<Args>()...), void())> If MF is a has_to_* that doesn't have a parameterized constructor! So what is MF(Args...) doing?Urolith
@JonathanMee MF(Args...) is a function type, like the type of a function MF my_function(Args...); Function types can be used to compose types in template arguments. In this case, we compose a metafunction type and the types that shall be used as the metafunction's arguments, without forcing evaluation of the metafunction. We could as well pass a tuple<MF, Args...>. The models class template then separates the metafunction from the arguments, and tries to evaluate it (MF{}.requires_(declval<Args>()...)).Tycoon
@Tycoon If MF is a function type, what in the world is this: MF{}.requires_(declval<Args>()...)? There MF is behaving like an object. I don't get it. What is MF?Urolith
@JonathanMee MF is not a function type. MF(int, double) is a function type. E.g. struct MF {}; MF my_function(int, double); then the type of my_function is MF(int, double) and the type of a pointer to my_function is MF(*)(int double). MF{} creates an object of type MF, and we're calling its member function requires_ to see if that compiles. In order to pass both MF and the argument types as a single template argument, we need to store both in a single type. This can be a tuple<MF, Args...> or a function type like MF(Args...).Tycoon
With clang, you get better error messages with std::enable_if than with std::enable_if_t. This is why a macro for std::enable_if is preferred.Michaud
M
2

Well, you can just skip all the metaprogramming magic and use the fit::conditional adaptor from the Fit library:

FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional(
    [](auto x) -> decltype(to_string(x))
    {
        return to_string(x);
    },
    [](auto x) -> decltype(static_cast<ostringstream&>(ostringstream() << x).str())
    {
        return static_cast<ostringstream&>(ostringstream() << x).str();
    }
);

Or even more compact, if you don't mind macros:

FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional(
    [](auto x) FIT_RETURNS(to_string(x)),
    [](auto x) FIT_RETURNS(static_cast<ostringstream&>(ostringstream() << x).str())
);

Note, I also constrained the second function as well, so if the type can't be called with to_string nor streamed to ostringstream then the function can't be called. This helps with better error messages and better composability with checking type requirements.

Michaud answered 28/5, 2015 at 19:49 Comment(4)
Interesting, I'm unfamiliar with "Fit". What is it exactly?Urolith
@JonathanMee It is a function utility library for C++11/14. I linked to it in my answer.Michaud
Interesting, how does it compare to the ubiquitous Boost?Urolith
Its in the boost library incubator: rrsd.com/blincubator.com/alphabeticallyMichaud
G
0

My take: to universally determine if something is callable without making verbose type traits for each and every one, or using experimental features, or long code:

template<typename Callable, typename... Args, typename = decltype(declval<Callable>()(declval<Args>()...))>
std::true_type isCallableImpl(Callable, Args...) { return {}; }

std::false_type isCallableImpl(...) { return {}; }

template<typename... Args, typename Callable>
constexpr bool isCallable(Callable callable) {
    return decltype(isCallableImpl(callable, declval<Args>()...)){};
}

Usage:

constexpr auto TO_STRING_TEST = [](auto in) -> decltype(std::to_string(in)) { return {}; };
constexpr bool TO_STRING_WORKS = isCallable<Input>(TO_STRING_TEST);
Godless answered 28/8, 2020 at 8:13 Comment(0)
W
0

I find concepts of C++20 easy to read. We can write:

#include<concepts>

template<typename T>
concept has_to_string = requires (T a){ std::to_string(a);};

template<typename T>
auto stringify(T a){
    return "Doesn't have to_string";
}

template<has_to_string T>
auto stringify(T a){
    return "Has to_string";
}

And we can test it like:

int main()
{
    int a;
    int b[2];
    std::cout<<stringify(a); // Has to_string
   std::cout<<stringify(b); // Doesn't have to_string
}

Compiler GCC 10.2 flag -std=c++20.

Westcott answered 26/10, 2021 at 7:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.