Should decltype on a template value parameter trigger a SFINAE context?
Asked Answered
C

1

7

While experimenting with some template constraint constructs, I encountered a surprising behavior in Clang 3.7:

struct constraint_success {};
struct constraint_failure {};

template<bool>
struct require_t {};

template<>
struct require_t<true> {
    static constexpr constraint_success* result = nullptr;
};

template<>
struct require_t<false> {
    static constexpr constraint_failure* result = nullptr;
};

template<bool Condition>
constexpr auto require = require_t<Condition>::result;

//A named dummy value, since we need a default
constexpr constraint_success* required = nullptr;

This decltype triggers a SFINAE context in my compiler:

template<constraint_success* value>
using constraint = decltype(value);

As opposed to:

//template<constraint_success* value>
//using constraint = constraint_success*;

Example:

//define custom constraints
template<typename T>
constexpr auto Pointer = require<std::is_pointer<T>::value>;

template<typename T>
constexpr auto NotPointer = require<!std::is_pointer<T>::value>;

//constrain template parameters
template<typename T, constraint<Pointer<T>> = required>
void foo() {
    std::cout << "Hello, pointer!\n";
}

template<typename T, constraint<NotPointer<T>> = required>
void foo() {
    std::cout << "Hello, not pointer!\n";
}

int main() {
    foo<int*>();
    foo<int>();
    return 0;
}

Is this required by the standard, or is this a "lucky" compiler bug?

Wandbox link

Citadel answered 10/1, 2016 at 13:31 Comment(0)
C
9

Alias templates such as constraint are substituted within any template definition they're used in. They're special in this way: other templates are substituted once definite arguments are supplied.

So, this declaration:

template<typename T, constraint<Pointer<T>> = required>
void foo();

is roughly equivalent to this declaration (the difference being substitution of a static_cast for an implicit conversion):

template<typename T, decltype(static_cast<constraint_success*>
                                         (require_t<std::is_pointer<T>>::result))
                              = required>
void foo();

This produces SFINAE when the static_cast is invalid.

The effect is useful, and essentially allows you to emulate constraints from the upcoming Concepts feature. However, this particular approach is rather convoluted, and you're not really leveraging it.

The canonical SFINAE approach is this:

template< typename T >
std::enable_if_t< std::is_pointer< T >::value > foo();

You can be a little more clean and clever, and remove the SFINAE from the return type — what you seem to be currently going for:

template< typename T,
    std::enable_if_t< std::is_pointer< T >::value > * = nullptr >
void foo();

Given the variable templates in the Library Fundamentals TS, this corresponds symbol-for-symbol with your example:

template< typename T,
    std::enable_if_t< std::is_pointer_v< T > > * = nullptr >
void foo();

 // constraint      < Pointer          <T>>      = required>

The really Concepts-ish way is like this:

template< typename T, std::enable_if_t< std::is_pointer< T >::value > * = nullptr >
using Pointer = T;

template< typename T >
void foo( Pointer<T> );
Contestation answered 10/1, 2016 at 14:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.