39

I'm working on upgrading some C++ code to take advantage of the new functionality in C++11. I have a trait class with a few functions returning fundamental types which would most of the time, but not always, return a constant expression. I would like to do different things based on whether the function is constexpr or not. I came up with the following approach:

template<typename Trait>
struct test
{
    template<int Value = Trait::f()>
    static std::true_type do_call(int){ return std::true_type(); }

    static std::false_type do_call(...){ return std::false_type(); }

    static bool call(){ return do_call(0); }
};

struct trait
{
    static int f(){ return 15; }
};

struct ctrait
{
    static constexpr int f(){ return 20; }
};

int main()
{
   std::cout << "regular: " << test<trait>::call() << std::endl;
   std::cout << "constexpr: " << test<ctrait>::call() << std::endl;
}

The extra int/... parameter is there so that if both functions are available after SFINAE, the first one gets chosen by overloading resolution.

Compiling and running this with Clang 3.2 shows:

regular: 0
constexpr: 1

So this appears to work, but I would like to know if the code is legal C++11. Specially since it's my understanding that the rules for SFINAE have changed.

Xeo
  • 129,499
  • 52
  • 291
  • 397
K-ballo
  • 80,396
  • 20
  • 159
  • 169
  • Interesting problem. I wrote an answer which I thought was proving it is legal, but then I wrote a [modified version](http://liveworkspace.org/code/SWmBI$5) that should have been equally valid according to my answer, and that one doesn't compile on any compiler. So I'm not going to submit the answer, but I am very curious. – Andy Prowl Mar 05 '13 at 22:03
  • 3
    Related: [Is is_constexpr possible in C++11?](http://stackoverflow.com/questions/13299394/is-is-constexpr-possible-in-c11). Also, see [Calling constexpr in default template argument](http://stackoverflow.com/questions/10721130/calling-constexpr-in-default-template-argument). – Jesse Good Mar 05 '13 at 22:10
  • @AndyProwl: I'm curious, could you maybe post your solution on liveworkspace.org or other similar site? – K-ballo Mar 05 '13 at 22:12
  • @K-ballo: If you mean the answer, yes sure. I will post it here and delete it, you'll be able to see it anyway. – Andy Prowl Mar 05 '13 at 22:52
  • @K-ballo: Also (in case you missed it) the previous comment contains a link to the modified version I was mentioning. – Andy Prowl Mar 05 '13 at 22:59
  • 1
    @andy i think your thought error was to assume that the substitution during deduction would substitute into the default template argument. but if you look closer, you find that the default template argument was already substituted by one level higher, the surrounding class template. when instantiating member declarations of a class template specialization, default template arguments of member templates are not instantiated immediately, but only when used. That however doesn't mean that the instantiation of them at the point of use is an SFINAE context. – Johannes Schaub - litb Mar 05 '13 at 23:00
  • havent yet checked the spec in detail to see whether there is some subtle rules that define the behavior. but i seem to remember from an earlier reading that this is convoluted. – Johannes Schaub - litb Mar 05 '13 at 23:01
  • @JohannesSchaub-litb: Are you referring to my answer, which tries to prove the OP's code is legal, or to my modified version of the OP's code, which I expected to compile and doesn't? – Andy Prowl Mar 05 '13 at 23:14
  • @Andy: The code you posted in the comment. The substitution is done at the wrong level. – Xeo Mar 05 '13 at 23:15
  • @Xeo: I'm sure you guys are right, but I think I miss some knowledge to actually understand your explanations – Andy Prowl Mar 05 '13 at 23:16
  • More generally, you might want to change that template to `template` so that the test works regardless of the function return type – bremen_matt May 09 '19 at 13:24

2 Answers2

14

NOTE: I opened a question here about whether OPs code is actually valid. My rewritten example below will work in any case.


but I would like to know if the code is legal C++11

It is, although the default template argument may be considered a bit unusual. I personally like the following style better, which is similar to how you (read: I) write a trait to check for a function's existence, just using a non-type template parameter and leaving out the decltype:

#include <type_traits>

namespace detail{
template<int> struct sfinae_true : std::true_type{};
template<class T>
sfinae_true<(T::f(), 0)> check(int);
template<class>
std::false_type check(...);
} // detail::

template<class T>
struct has_constexpr_f : decltype(detail::check<T>(0)){};

Live example.


Explanation time~

Your original code works because a default template argument's point of instantiation is the point of instantiation of its function template, meaning, in your case, in main, so it can't be substituted earlier than that.

§14.6.4.1 [temp.point] p2

If a function template [...] is called in a way which uses the definition of a default argument of that function template [...], the point of instantiation of the default argument is the point of instantiation of the function template [...].

After that, it's just usual SFINAE rules.


† Atleast I think so, it's not entirely clear in the standard.

Community
  • 1
  • 1
Xeo
  • 129,499
  • 52
  • 291
  • 397
  • That quote from the standard clears all my doubts, thank you. – K-ballo Mar 06 '13 at 05:19
  • unfortunately i dont think it is that clear. a "default template argument" could not be specified for functions in C++03, yet that text was present there too. the text only talks about default arguments on functions, and not on class templates. which makes me believe it doesnt talk about template arguments, but function arguments. – Johannes Schaub - litb Mar 06 '13 at 08:35
  • i disagree that this code is clearly valid since it is not clear that the error is in an immediate context and it is not at all clear whether delayed instantiation happens at all. – Johannes Schaub - litb Mar 06 '13 at 08:37
  • @Johannes: Ooh, good point. I kinda had my doubt if OP's code was actually valid, until I stumbled opon that part of the standard. Mind joining me in the Lounge to discuss this? – Xeo Mar 06 '13 at 09:23
  • @xeo im in the bus on the way to work. i wrote my comments when i still was getting up on my phone. so unfortunately i can't join the lounge :-) – Johannes Schaub - litb Mar 06 '13 at 09:30
  • what is clear is that the standard says that for the purpose of instantiation, default function arguments of temploids are considered to be template definitions (and thereby gain independent instantiation capability). – Johannes Schaub - litb Mar 06 '13 at 09:36
  • @Johannes: I wasn't sure what exactly it means for a "default template argument" to also be a definition. This is complicated. :| – Xeo Mar 06 '13 at 10:01
  • no. default function arguments are considered to be definitions. not default template arguments, as far as i am aware. i recommend to post a new question to clarify this matter. – Johannes Schaub - litb Mar 06 '13 at 10:51
  • @Johannes: Done, see the note at the top of the answer. – Xeo Mar 06 '13 at 13:47
4

Prompted by @marshall-clow, I put together a somewhat more-generic version of an type-trait for detecting constexpr. I modelled it on std::invoke_result, but because constexpr depends on the inputs, the template arguments are for the values passed in, rather than the types.

It's somewhat limited, as the template args can only be a limited set of types, and they're all const when they get to the method call. You can easily test a constexpr wrapper method if you need other types, or non-const lvalues for a reference parameter.

So somewhat more of an exercise and demonstration than actually-useful code.

And the use of template<auto F, auto... Args> makes it C++17-only, needing gcc 7 or clang 4. MSVC 14.10.25017 can't compile it.

namespace constexpr_traits {

namespace detail {

// Call the provided method with the provided args.
// This gives us a non-type template parameter for void-returning F.
// This wouldn't be needed if "auto = F(Args...)" was a valid template
// parameter for void-returning F.
template<auto F, auto... Args>
constexpr void* constexpr_caller() {
    F(Args...);
    return nullptr;
}

// Takes a parameter with elipsis conversion, so will never be selected
// when another viable overload is present
template<auto F, auto... Args>
constexpr bool is_constexpr(...) { return false; }

// Fails substitution if constexpr_caller<F, Args...>() can't be
// called in constexpr context
template<auto F, auto... Args, auto = constexpr_caller<F, Args...>()>
constexpr bool is_constexpr(int) { return true; }

}

template<auto F, auto... Args>
struct invoke_constexpr : std::bool_constant<detail::is_constexpr<F, Args...>(0)> {};

}

Live demo with use-cases on wandbox

Community
  • 1
  • 1
TBBle
  • 1,436
  • 10
  • 27
  • How can `template()>` be legal, since the third parameter is after the ellipsis ? – xryl669 Jan 29 '21 at 13:18
  • 2
    From [cppreference](https://en.cppreference.com/w/cpp/language/parameter_pack): "In a primary class template, the template parameter pack must be the final parameter in the template parameter list. In a function template, the template parameter pack may appear earlier in the list provided that all following parameters can be deduced from the function arguments, or have default arguments". Basically, arguments after the pack can not get filled in by the arguments to the instantiation, but are allowed if they are deduced or defaulted. This only works for function templates. – TBBle Feb 05 '21 at 09:33