c++sfinaestatic-assert

Why is static_assert breaking substitution?


Please, consider the following C++14 code:

#include <type_traits>

template<typename T>
class Bar {
    static_assert(std::is_destructible<T>::value, "T must be destructible");
};

template<typename T>
void foo(Bar<T> const &) {}

template<typename T>
void foo(T const &) {}

class Product {
public:
    static Product *createProduct() { return new Product{}; }
    static void destroyProduct(Product *product) { delete product; }
private:
    Product() = default;
    ~Product() = default;
};

int main()
{
    Product* p = Product::createProduct();
   
    foo(*p);            // call 1: works fine
    foo<Product>(*p);   // call 2: fails to compile
       
    Product::destroyProduct(p);
           
    return 0;
}

And the error message from clang:

error: static_assert failed due to requirement 'std::is_destructible<Product>::value' "T must be destructible"
    static_assert(std::is_destructible<T>::value, "T must be destructible");
    ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
note: in instantiation of template class 'Bar<Product>' requested here
    foo<Product>(*p);   // call 2: fails to compile
                 ^
note: while substituting deduced template arguments into function template 'foo' [with T = Product]
    foo<Product>(*p);   // call 2: fails to compile
    ^
1 error generated.

My understanding is that both call 1 and call 2 should compile fine, but call 2 fails to compiler not only on clang but also on gcc and msvc.

From the standard perspective, is it correct that call 1 succeeds and call 2 fails to compile? Why?

Note: I know that I can work around the error by adding a std::enable_if to the first overload of foo, but I'd like to understand why call 1 is OK but call 2 is not.


Solution

  • Substitution failure is not an error is limited. If failure occurs beyond the "immediate context", the failure is hard and not prevented by SFINAE.

    When you type foo<Product>(?), it creates an overload set first. This overload set includes both overloads of foo. But creating the first overload of foo leads to a static_assert failure outside of the immediate context, so a hard error.

    When you type foo(?) it also tries to create an overload set. Both of the templates are considered. It has no T being passed in, so it attempts to deduce the T from the arguments.

    The argument is a Product&. Deducing T const& from a Product& produces T=Product const. Deducing Bar<T> const& from a Product& ... fails to deduce a T, as neither Product nor any of its base classes are produced from a template of the form Bar<typename>.

    Without a T deduced, the foo(Bar<T> const&) overload is discarded. There is no way for T=Product to be deduced, so no hard error from attempting to instantiate foo(Bar<Product> const&) could occur.

    In short, for call 2, you manually force a Bar<Product> possibility into existence. For call 1, the compiler attempt to deduce the template arguments never gets there.

    As a thought experiment, consider changing the declaration of Product to:

    class Product: public Bar<int> {
    

    and leave everything else the same.

    Now

    foo(*p)
    

    will generate 2 overload to consider - one

    template<class T=Product const>
    void foo(Product const&)
    

    and one

    template<class T=int>
    void foo(Bar<int> const&)
    

    with the second one being generated by looking at the base classes of Product and finding a Bar<int>. T is deduced to Product in one case, and int in the other.

    But when you do foo<Product> you aren't relying on deduction to find T - you are setting it manually. There isn't any deduction left to do.