Home > Software design >  Contradictory SFINAE on constructor using std::is_constructible
Contradictory SFINAE on constructor using std::is_constructible

Time:10-05

Why is the following code behaving as commented?

struct S
{
    template<typename T, typename = std::enable_if_t<!std::is_constructible_v<S, T>>>
    S(T&&){}
};

int main() {
    S s1{1}; // OK
    int i = 1;
    S s2{i}; // OK
    static_assert(std::is_constructible_v<S, int>); // ERROR (any compiler)
}

I get that for the constructor to be enabled, the assertion must be false. But S still is constructed from int in the example above! What does the standard say and what does the compiler do?

I would assume that before enabling the template constructor, S in not constructible so std::is_constructible<S, int> instantiates to false. That enables the template constructor but also condemns std::is_constructible<S, int> to always test false.


I've also tested with my own (pseudo?) version of std::is_constructible:

#include <type_traits>

template<typename, typename T, typename... ARGS>
constexpr bool isConstructibleImpl = false;

template<typename T, typename... ARGS>
constexpr bool isConstructibleImpl<
    std::void_t<decltype(T(std::declval<ARGS>()...))>,
    T, ARGS...> =
    true;

template<typename T, typename... ARGS>
constexpr bool isConstructible_v = isConstructibleImpl<void, T, ARGS...>;

struct S
{
    template<typename T, typename = std::enable_if_t<!isConstructible_v<S, T>>>
    S(T&&){}
};

int main() {
    S s1{1}; // OK
    int i = 1;
    S s2{i}; // OK
    static_assert(std::is_constructible_v<S, int>); // OK
}

I suppose that it is because now std::is_constructible is not sacrificed for SFINAE in the constructor. isConstructible is sacrificed instead.

That brings me to a second question: Is this last example a good way to perform SFINAE on a constructor without corrupting std::is_constructible?


Rational: I ended up trying that SFINAE pattern to tell the compiler not to use the template constructor if any of the other available constructors matches (especially the default ones), even imperfectly (e.g., a const & parameter should match a & argument and the template constructor should not be considered a better match).

CodePudding user response:

Your first code example is undefined behaviour, because S is not a complete type within the declaration of itself. std::is_constructible_v however requires all involved types to be complete:

See these paragraphs from cppreference.com:

T and all types in the parameter pack Args shall each be a complete type, (possibly cv-qualified) void, or an array of unknown bound. Otherwise, the behavior is undefined.

If an instantiation of a template above depends, directly or indirectly, on an incomplete type, and that instantiation could yield a different result if that type were hypothetically completed, the behavior is undefined.

This makes sense because in order for the compiler to know if some type can be constructed it needs to know the full definition. In your first example, the code is kind of recursive: the compiler needs to find out if S can be constructed from T by checking the constructors of S which themselves depend on is_constructible etc.

  • Related