Why class template instantiation fails for unused member template function and implicit instantiation of member function's declaration
Asked Answered
K

3

6

Whats wrong with this:

#include <type_traits>

struct A;

template<typename T>
struct B
{
    template<typename=std::enable_if<std::is_copy_constructible<T>::value>>
    void f1() {}
};

template<typename T>
struct C {};


// Type your code here, or load an example.
int main() {
    // Following fails
    B<A> b;
    // Could use this:
    // b.f1<C>();

    // This complies
    C<A> c;

    return 0;
}

/* This to be in or not doesn't make a difference
struct A
{};
*/

I tried this here: https://godbolt.org/z/NkL44s with different compilers:

  • x86-64 gcc 9.2: compiles
  • x86-64 gcc (trunk): fails
  • x86-64 clang 6.0.0: compiles
  • x86-64 clang 7.0.0 and later: fails
  • x64 msvc v19.22: compiles
  • x64 msvc v19.23 (tested internally): fails

So why do more recent compilers reject this? When instantiating B<A> it is not clear in which form f1 will be used or if it will be used at all. So why the compiler complains about it? Shouldn't the f1 member template function be checked only if it is really used?


Edit:
As mentioned in comments, I made an unintented mistake in above code: std::enable_if should have been std::enable_if_t, as in this corrected playground: https://godbolt.org/z/cyuB3d

This changes the picture of compilers passing this code without error:

  • gcc: fails
  • clang: fails
  • x64 msvc v19.22: compiles
  • x64 msvc v19.23 (tested internally): fails

However, the question remains: Why does a defaulted template parameter of a function that is never used lead to compilation failure?

Kabob answered 10/10, 2019 at 10:5 Comment(9)
It's typename = std::enable_if_t<...>, not typename = std::enable_if<...>.Jemena
can you include an error message? Are all compilers complaining in the same way?Extravagate
A simplified case: template<bool = std::is_copy_constructible_v<T>> void f1() {}. The problem seems to be related to the time of resolution of default template arguments. Note that T being incomplete type results in undefined behaior. The differences I can observe are caused by compiler checks for type completeness.Greatcoat
@L.F. I don't think it matters here. SFINAE is not applied and the anonymous template parameter may as well refer to the std::enable_if type itself.Greatcoat
I added a paragraph to the post responding to the correctly spotted misuse of std::enable_if versus std::enable_if_t.Kabob
@JojakimStahl Note that this is not a correct application of SFINAE. Read, e.g., Why doesn't SFINAE (enable_if) work for member functions of a class template?.Greatcoat
@DanielLangr Note that this is not the same case as in the question you referred to, where the functions are all completely defined with instatiation of the template class. In my example, it is a template function, which IMHO the compiler can't instantiate until it is used. When using the function, I could pass a template parameter in which case the default parameter has no importance at all.Kabob
@JojakimStahl Sure, my comment was about a different problem. I think your problem with seemingly-unnecessary instantiation of a default template argument is not related to SFINAE at all.Greatcoat
Not sure if it is what you are asking, but this use of enable_if is suspicious. As a rule of thumb, enable_if condition has to depend on a type deduced in the same template “line”.Plankton
I
2

When instantiating the class template B<A>, the compiler instantiates the declaration, but not the definition of B<A>::f1:

The implicit instantiation of a class template specialization causes

  • the implicit instantiation of the declarations, but not of the definitions, of the non-deleted class member functions, member classes, scoped member enumerations, static data members, member templates, and friends; and
  • [...]

The implicit instantiation of a class template specialization does not cause the implicit instantiation of default arguments or noexcept-specifiers of the class member functions.

- [temp.inst] p3

However, that just means that the function body isn't instantiated; the rest is. If f1 had default arguments, those wouldn't be instantiated, but the std::enable_if is a default template argument so this exception doesn't apply.

The following gets instantiated:

template<typename = std::enable_if<std::is_copy_constructible<A>::value>>
void f1() {}

The default template argument is ill-formed because std::is_copy_constructible requires:

T shall be a complete type, cv void, or an array of unknown bound.

Solution

In C++20, using a trailing requires-clause and the std::copy_constructible concept:

void f1() requires std::copy_constructible<T> {}

In C++17:

template<typename U = T>
std::enable_if_t<std::is_copy_constructible_v<U>> f1() {}

This works because std::enable_if_t depends on a template parameter to the current function template, so SFINAE can take place as intended.

See std::enable_if to conditionally compile a member function for more strategies.

Irradiation answered 31/12, 2023 at 9:54 Comment(0)
J
3

The reason is that std::is_constructible requires a complete type: (Table 42)

Template

template <class T>
struct is_­copy_­constructible;

Preconditions

T shall be a complete type, cv void, or an array of unknown bound.

Failing to meet a library "shall" requirement results in undefined behavior.

Jemena answered 10/10, 2019 at 10:13 Comment(7)
just like my answer you also miss to explain why the error in the method matters when one can expect that it is actually not instantiatedExtravagate
@formerlyknownas_463035818 Hmm, I'm not sure either. It seems that the default argument should not be instantiated ...Jemena
It's weird that the whole section temp.inst mentions default template argument only once — in the context of variable templates. I can't find anything in the Standard to specify when default template arguments should be instantiated.Greatcoat
@DanielLangr Yes, how template declarations themselves are instantiated seems underspecified.Jemena
Anyway, I believe that in all cases, compilers do instantiate the default template argument. The only difference is whether they check for T begin complete type in std::is_copy_constructible type trait. For instance, this check was added into libstdc++ in May, 2019, i.e., relatively recently; see this commit, namely these added lines.Greatcoat
Replacing the template declaration of f1 with template<typename T2=T, typename=std::enable_if_t<std::is_copy_constructible<T2>::value>> makes it compilable.Kabob
@JojakimStahl Apparently the default argument can't be instantiated if it depends on a template parameter (that's how SFINAE works). The other case is more difficult - I guess this has something to do with immediate contexts, but I can't say for sure ..Jemena
E
2

From cppreference on is_copy_constructible<T>:

T shall be a complete type, (possibly cv-qualified) void, or an array of unknown bound. Otherwise, the behavior is undefined.

So it seems like you have just plain UB in older compiler versions while the newer ones are nice enough to tell you that A has to be a complete type.

Note that in the presence of UB, compliers are not reuqired to issue an error, but they may do it, which is a nice thing.

Extravagate answered 10/10, 2019 at 10:14 Comment(0)
I
2

When instantiating the class template B<A>, the compiler instantiates the declaration, but not the definition of B<A>::f1:

The implicit instantiation of a class template specialization causes

  • the implicit instantiation of the declarations, but not of the definitions, of the non-deleted class member functions, member classes, scoped member enumerations, static data members, member templates, and friends; and
  • [...]

The implicit instantiation of a class template specialization does not cause the implicit instantiation of default arguments or noexcept-specifiers of the class member functions.

- [temp.inst] p3

However, that just means that the function body isn't instantiated; the rest is. If f1 had default arguments, those wouldn't be instantiated, but the std::enable_if is a default template argument so this exception doesn't apply.

The following gets instantiated:

template<typename = std::enable_if<std::is_copy_constructible<A>::value>>
void f1() {}

The default template argument is ill-formed because std::is_copy_constructible requires:

T shall be a complete type, cv void, or an array of unknown bound.

Solution

In C++20, using a trailing requires-clause and the std::copy_constructible concept:

void f1() requires std::copy_constructible<T> {}

In C++17:

template<typename U = T>
std::enable_if_t<std::is_copy_constructible_v<U>> f1() {}

This works because std::enable_if_t depends on a template parameter to the current function template, so SFINAE can take place as intended.

See std::enable_if to conditionally compile a member function for more strategies.

Irradiation answered 31/12, 2023 at 9:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.