8

The follow code does not compile because struct A doesn't support the -- operator.

struct A {};

struct B {
  void Run() {}
  A& Dec(A& a) { return --a; }
};

int main(int argc, char** argv) {
  B b;
  b.Run();
}

Same with this code.

struct A {};

template <class T>
struct B {
  void Run() {}
  A& Dec(A& a) { return --a; }
};

int main(int argc, char** argv) {
  B<A> b;
  b.Run();
}

So why does this compile (in C++11)?

struct A {};

template <class T>
struct B {
  void Run() {}
  T& Dec(T& a) { return --a; }
};

int main(int argc, char** argv) {
  B<A> b;
  b.Run();
}

It appears that instantiating the template does not automatically instantiate unused methods within the template that depend on the type parameter to type-check, which means that the template will match even if some of its methods don't. It's disappointing because I was hoping to use SFINAE to detect the applicability of various methods and operators to types, but if template substitution succeeds even when calls to the methods would be compile-time errors, the technique won't work.

user7610
  • 25,267
  • 15
  • 124
  • 150

1 Answers1

6

It was decided (by the C++ committee) that methods of template classes would only have their bodies instantiated if they where used.

This makes it easier to write some C++ code at the cost of hard errors when they are used.

As an example of this, std::vector uses it with std::vector::operator<; if you don't have a < calling it is an error. If you do, calling it works.

More modern C++ would encourage SFINAE disabling it so you can detect if < is safe or not, but that technique was not used back when std::vector was designed. You can see the evolution of this technique's use in std::function, which went from greedily consuming almost anything in a universal constructor to that constructor only being considered for overload resolution when it would work between C++11 and C++14.

If you want SFINAE, you cannot rely on the bodies of code like that. In order to lighten the load for compilers, compilers only have to examine declarations not definitions of functions when doing SFINAE tests.

Part of the reason is that SFINAE on expressions is hard; on entire bodies is harder. The compiler has to speculatively compile the body of the function, hit the error, then back out to the "nope, nothing was done" state.

Errors in the body of functions are always hard errors. You cannot avoid this in the current version of C++.

Now, you can write functions which decide if there will be errors or not, but don't actually have an error, then use their bodies to determine if other code will error. For example:

template<class T>
auto foo() {
  constexpr if(sizeof(T)<4) {
    return std::true_type{};
  } else {
    return std::false_type{};
}

you can use foo<char>() in some SFINAE somewhere, and its true or false-ness can make another overload substitution fail or not.

Note that the error (if any) still happens outside the body of a function (foo here).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    Note that the committee's decision happened largely because some early template implementations did pretty much the opposite (an instantiation would only compile if all the template's members would work for that instantiation) and that turned out to work quite poorly--it prevented templates from being instantiated in a lot of cases that people now take for granted will work fine. Just for example, `vector` supports move-only types, even though it obviously includes code that uses copying. – Jerry Coffin Jul 17 '17 at 18:41