Why is static_assert breaking substitution?

109 Views Asked by At

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.

2

There are 2 best solutions below

0
Yakk - Adam Nevraumont On BEST ANSWER

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.

0
Jarod42 On

In foo(*p), as Product is not a Bar<T>, no void foo(Bar<T> const &) is instantiated.

The only possible overload is template<typename T> void foo(T const &).

In foo<Product>(*p), you specify template parameter, so there are following candidate

  • foo<Product>(const Bar<Product>&)
  • foo<Product>(const Product&)

To know if Product can be converted to Bar<Product>, you have to instantiate it , producing an hard error.

Compiler might instantiate that template, even if it would not be taken by overload resolution.

foo (might) instantiate Bar which might be a match (but it is not). But Bar leads to an hard error.