3
$\begingroup$

SFINAE stands for Substitution Failure Is Not An Error. It's widely used by C++ in template resolution, where it has two major consequences:

  1. If a specific template instantiation is invalid, but there exists a different overload that would be valid, no error is reported: the invalid overload is ignored.

    #include <string>
    
    template<class T>
    void makeFive(T& x) {
      x = 5;
    }
    
    template<class T>
    void makeFive(typename T::size_type& x){
      x = 5;
    }
    
    int main() {
      size_t x;
      makeFive<size_t>(x); // OK: uses first overload
      makeFive<std::string>(x); // OK: uses second overload
    }
    
  2. Default parameters don't cause errors if they aren't used.

    #include <iostream>
    #include <string>
    
    template<class T>
    void print(T value = 5) {
      std::cout << value << std::endl;
    }
    
    int main() {
      print<int>(); // OK: Default value is an integer.
      print<std::string>("Hello, World!"); // OK: Default value is unused.
      print<std::string>(); // Error: Default value is not a string.
    }
    

The first usage is largely unnecessary in modern languages because of interfaces and traits, but the second one is occasionally helpful. For example, when recreating if statements in Swift, I tried to write

func `if`<T>(_ condition: Bool, _ ifTrue: () -> T, else ifFalse: () -> T = { () -> Void in () }) -> T {
  condition ? ifTrue() : ifFalse()
}

However, Swift lacks SFINAE, so the default value for ifFalse causes an error.

What are the pros and cons to SFINAE? Why would a language with generics choose to have or not have them?

$\endgroup$
2
  • $\begingroup$ With the addition of concepts the use cases for SFINAE in C++ are significantly reduced. $\endgroup$ Commented Jun 16, 2023 at 9:28
  • $\begingroup$ I would say one negative consequence of being able to do it is there is less incentive to add something better to a language like concepts or refinement types. $\endgroup$ Commented Jun 16, 2023 at 9:29

3 Answers 3

4
$\begingroup$

A major problem with SFINAE is that it becomes very difficult to extend the language without any possibility of breaking existing programs. If a construct cannot appear within a valid program, then specifying a meaning for that construct and allowing programs to contain that construct would not affect the behavior of any existing valid programs, because the construct could not appear in such programs. SFINAE breaks that principle, however, since it's possible for invalid constructs to appear in valid programs, in contexts where defining a valid meaning for the construct would change the behavior of the programs in question.

$\endgroup$
2
$\begingroup$

SFINAE is a bit strange. It's only a thing in statically typed languages, because if you have dynamic types then you don't need generics. But then when you write a template, the template itself isn't type-checked, because the correctness of the template code depends on what type the parameter T gets realised as.

In other languages, if the correctness of a generic function depends on T meeting certain conditions, then those conditions are expressed statically in the function declaration, e.g. T extends Foo. This allows the function declaration to be type-checked using the knowledge of those constraints.

So SFINAE is sort of like "dynamic typing" at a meta-level; the types won't be checked until you call the function, albeit "when you call the function" is still at compile-time rather than runtime. The upside, I suppose, is that (like dynamic types) you can write code that works if you use it properly, without having to express in the type system itself (via extends constraints) which uses are "proper". This might be useful if the language is not expressive enough to describe what the actual constraints are; but generally speaking, users of a statically typed language would prefer for the type system to be expressive enough instead.

I'll note that your second use-case is actually possible without SFINAE, if you allow function overloads like in Typescript (example):

function if_(condition: boolean, ifTrue: () => void): void;
function if_<T>(condition: boolean, ifTrue: () => T, ifFalse: () => T): T;
function if_(condition: boolean, ifTrue: () => unknown, ifFalse = () => {}) {
    return condition ? ifTrue() : ifFalse();
}

Or, of course, you could just do it without overloads by giving a different name to the version of the function which has a default argument.


A more minor issue is that type-checking after substitutions means more work for the type checker, and hence slower compile times, because the template code has to be checked every time it gets used. (On the other hand, if a template gets used zero times then it doesn't ever have to be checked, so compiling in the presence of unused code is possibly faster.)

Also, errors are reported less helpfully at the call-site even if the mistake occurs in the template itself ─ and the errors are in the generated code rather than in the user's actual code, making them potentially less informative.

$\endgroup$
2
$\begingroup$

One problem is, it doesn't cause an error, but doesn't specify exactly why it isn't an error. If someone uses someone else's libraries without knowing how they were implemented, in situations such as defining a class implementing multiple interfaces from different sources, they may accidentally make some presumably invalid template applications valid, and break existing code.

This might be fixed by some other features, but with the other features, it would also be likely to remove the need of SFINAE at all.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .