Russell's paradox in C++ templates [duplicate]
Asked Answered
A

1

21

Consider this program:

#include <iostream>
#include <type_traits>

using namespace std;

struct russell {
    template <typename barber, 
              typename = typename enable_if<!is_convertible<barber, russell>::value>::type>
    russell(barber) {}
};

russell verify1() { return 42L; }
russell verify2() { return 42; }

int main ()
{
    verify1();
    verify2();
    cout << is_convertible<long, russell>::value;
    cout << is_convertible<int, russell>::value;
    return 0;
}

If some type barber is not convertible to russell. we attempt to create a paradox by making it convertible (enabling a converting constructor).

The output is 00 with three popular compilers, though constructors are evidently working.

I suspect the behaviour should be undefined, but cannot find anything in the standard.

What should the output of this program be, and why?

Aloud answered 27/8, 2017 at 16:45 Comment(5)
RelatedVivl
@Vivl yeah this looks dupe enough....Aloud
@Vivl I sneakily added my answer, because I panicked after looking at this for ages and finally finishing the answer. I could rewrite it to fit your question and post it there instead, if you reckon that's more sensible.Applique
@Applique I don't know what's more sensible, but pretty sure I will upvote whatever you decide to do. Might even want to reverse the dupe direction?Vivl
@Vivl I intentionally duped it again, because your question has an undeniable resemblance. I'd leave it as it is now.Applique
A
8

During overload resolution, template argument deduction must instantiate the default argument to obtain a complete set of template arguments to instantiate the function template with (if possible). Hence the instantiation of is_convertible<int, russell> is necessitated, which internally invokes overload resolution. The constructor template in russell is in scope in the instantiation context of the default template argument.

The crux is that is_convertible<int, russell>::value evaluates the default template argument of russell, which itself names is_convertible<int, russell>::value.

is_convertible<int, russell>::value
              |
              v
russell:russell(barber)
              |
              v
is_convertible<int, russell>::value (not in scope)

core issue 287's (unadopted) resolution seems to be the de facto rule abode by major compilers. Because the point of instantiation comes right before an entity, value's declaration is not in scope while we're evaluating its initialiser; hence our constructor has a substitution failure and is_convertible in main yields false. Issue 287 clarifies which declarations are in scope, and which are not, namely value.

Clang and GCC do slightly differ on how they treat this situation. Take this example with a custom, transparent implementation of the trait:

#include <type_traits>

template <typename T, typename U>
struct is_convertible
{
    static void g(U);

    template <typename From>
    static decltype(g(std::declval<From>()), std::true_type{}) f(int);
    template <typename>
    static std::false_type f(...);

    static const bool value = decltype(f<T>()){};
};

struct russell
{
    template <typename barber,
              typename = std::enable_if_t<!is_convertible<barber, russell>::value>>
    russell(barber) {}
};

russell foo() { return 42; }

int main() {}

Clang translates this silently. GCC complains about an infinite recursion chain: it seems to argue that value is indeed in scope in the recursive instantiation of the default argument, and so proceeds to instantiate the initializer of value again and again. However, arguably Clang is in the right, since both the current and the drafted relevant phrase in [temp.point]/4 mandate that the PoI is before the nearest enclosing declaration. I.e. that very declaration is not considered to be part of the partial instantiation (yet). Kinda makes sense if you consider the above scenario. Workaround for GCC: employ a declaration form in which the name is not declared until after the initializer is instantiated.

enum {value = decltype(f<T>()){}};

This compiles with GCC as well.

Applique answered 28/8, 2017 at 19:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.