9

G++ and Clang++ agree that the following snippet is not valid C++:

template<int dim, int rank>
struct Tensor {};

template<int dim>
double InnerProduct(Tensor<dim, 1> const &, Tensor<dim, 1> const &)
  { return 0.0; }

template<int dim>
double DoubleInnerProduct(Tensor<dim, 2> const &, Tensor<dim, 2> const &)
  { return 0.0; }

template<int dim, int rank>
class Field
{
private:
  static double Dot(Tensor<dim, rank> const &u, Tensor<dim, rank> const &v) requires (rank == 1)
    { return InnerProduct(u, v); }

  static double Dot(Tensor<dim, rank> const &u, Tensor<dim, rank> const &v) requires (rank == 2)
    { return DoubleInnerProduct(u, v); }
};

template class Field<2, 1>;
template class Field<2, 2>;

The error message states that even the functions with unsatisfied constraints are instantiated:

error: no matching function for call to ‘DoubleInnerProduct(const Tensor<2, 1>&, const Tensor<2, 1>&)’
   22 |     { return DoubleInnerProduct(u, v); }

I can make it work in a number of ways (for example, declaring Dot as templates with a default parameter equal to rank to which the constraints are applied), but I expected it to work.

In general, shall I assume that template classes with member functions with constraints depending on their template parameters cannot be explicitly instantiated?

dfrib
  • 70,367
  • 12
  • 127
  • 192
metalfox
  • 6,301
  • 1
  • 21
  • 43
  • 1
    what is the error message? – 463035818_is_not_an_ai Jun 30 '20 at 14:29
  • 2
    Observations (not explanations): **(A)** If you just _declare_ the `Dot` overloads, the explicit instantiation declarations of `Field<2, 1>` and `Field<2, 2>` compiles successfully. **(B)** Even without removing the definitions, you may explicitly declare the full static member function viable for a given instantiation rather than just the instantiation declaration of the particular class template: `template double Field<2, 1>::Dot(Tensor<2, 1> const &, Tensor<2, 1> const &);`. – dfrib Jun 30 '20 at 14:46
  • 1
    [\[temp.explicit\]/10](https://eel.is/c++draft/temp.explicit#10) states that _"An explicit instantiation of a constrained template is required to satisfy that template's associated constraints. [...]"_ which may apply here, in the you can successfully supply and explicit instantiation definition for the correct `Dot` overload (as shown in my comment above). – dfrib Jun 30 '20 at 14:49
  • 1
    Moreover, [\[temp.explicit\]/12](https://eel.is/c++draft/temp.explicit#12) states that: _"An explicit instantiation definition that names a class template specialization explicitly instantiates the class template specialization and is an explicit instantiation definition **of only those members that have been defined at the point of instantiation**."_, which explains why Clang and GCC accepts an explicit instantiation definition of the `Field` class template given that the (static) member functions that for which the given instantiation is invalid are not yet defined. – dfrib Jun 30 '20 at 14:52
  • Is there a reason why you are explicitly instantiating with e.g. `template class Field<2, 2>;` rather than just declaring a variable `Field<2, 2> foo`? Is it just to understand what happens? – Arthur Tacca Jun 30 '20 at 16:09
  • @ArthurTacca Sometimes, when there are a finite number of possible template parameters, I place the implementation in a cpp file and use explicit template intantiation. – metalfox Jun 30 '20 at 16:12
  • @metalfox I see, that makes sense. – Arthur Tacca Jun 30 '20 at 16:19

2 Answers2

2

Explicit class template instantiation definitions are also explicit instantiation definitions of those members that have been defined at the point of instantiation

Consider the following simplified example:

template<int rank>
struct A {};

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { (void)(1); }
    void dot(A<rank>) requires (rank == 2) { (void)(2); }
};

[temp.explicit]/11 states [emphasis mine]:

An explicit instantiation that names a class template specialization is also an explicit instantiation of the same kind (declaration or definition) of each of its members (not including members inherited from base classes and members that are templates) that has not been previously explicitly specialized in the translation unit containing the explicit instantiation, provided that the associated constraints, if any, of that member are satisfied by the template arguments of the explicit instantiation ([temp.constr.decl], [temp.constr.constr]), except as described below. [...]

Which implies that an explicit instantiation definition that names only a class template specialization of Field, say

template struct Field<1>;

will also lead to the explicit instantiation definition of the dot overload which fulfills the constraint expression requires (rank == 1), but not for the overload with a constraint expression requires (rank == 2). However, the except as described below part leads us to [temp.explicit]/12, which states [emphasis mine]:

An explicit instantiation definition that names a class template specialization explicitly instantiates the class template specialization and is an explicit instantiation definition of only those members that have been defined at the point of instantiation.

Meaning that, for the simplified example above (followed by the explicit instantiation definition for Field<1>, as above), the passage above indicates the explicit instantiation definition of both dot overloads, as both have been defined at the point of the explicit instantiation definition of Field<1>. This, however, means an ODR-violation as there will be two definitions of Field<1>::void dot(A<1>).

// Not OK.
template<int rank>
struct A { };

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { (void)(1); }
    void dot(A<rank>) requires (rank == 2) { (void)(2); }
};

template struct Field<1>;

int main() {}

yielding the following error on Clang:

error: definition with same mangled name '_ZN5FieldILi1EE3dotE1AILi1EE' as  another definition
       void dot(A<rank>) requires (rank == 2) { }

Note that we may provide an explicit instantiation definition for, particularly, the dot non-template member of the Field class template for a given specialization of the latter, and GCC and Clang will happily accept it, indicating that the constraint expressions are respected when explicitly instantiating the overloaded, constrained functions:

// OK.
template<int rank>
struct A { };

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { (void)(1); }
    void dot(A<rank>) requires (rank == 2) { (void)(2); }
};

template void Field<1>::dot(A<1>);

int main() {}

but not when they are, as described above, implicitly given explicit instantiated definitions as per the [temp.explicit]/12 quote above, as this seems to provide separate instantiation definitions for both members (without respecting the constraint expression) and thus violating ODR.

The different behaviour from the compilers between the explicit instantiation definition of the class template specialization vs a non-template member function of the specialization is somewhat peculiar, but possibly the difference is that for the latter case, [temp.constr.constr]/2 applies [emphasis mine]

[...] Overload resolution requires the satisfaction of constraints on functions and function templates.


If we only declare but don't define the second overload, it will not be instantiated as part of the explicit instantiation definition (i.e., [temp.explicit]/12 does not apply for it) of Field<1>, and we will no longer have an ODR-violation:

// OK.
template<int rank>
struct A { };

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { (void)(1); }
    void dot(A<rank>) requires (rank == 2);
};

template struct Field<1>;

int main() {}

Now, why is this not failing for an implicit instantiation?

As per [temp.inst]/3 [emphasis mine]:

The implicit instantiation of a class template specialization causes

(3.1) 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 [...]

such that the following example is accepted by both Clang and GCC:

template<int rank>
struct A { };

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { (void)(1); }
    void dot(A<rank>) requires (rank == 2) { (void)(2); }
};

int main() { 
    Field<1> f{};
    (void)f;
}

where, as per [temp.inst]/4, the dot overloads will not be instantiated as the Field<1> specialization is not referenced in a context that requires their definitions to exist.

Finally, however, we may note that the implicit instantiation of the dot static member function of the Field class template will respect the constraint expression, and instantiate the overload which fulfills the constraint on the rank non template parameter of the particular class template specialization:

#include <iostream>

template<int rank>
struct A { };

template<int rank>
struct Field {
    void dot(A<rank>) requires (rank == 1) { std::cout << "1"; }
    void dot(A<rank>) requires (rank == 2) { std::cout << "2"; } 
};

int main() { 
    Field<1>{}.dot(A<1>{}); // "1"
}

This is likely governed by [temp.constr.constr]/2, as quoted above.

dfrib
  • 70,367
  • 12
  • 127
  • 192
  • 2
    *"SFINAE ... constraint expressions needs to be applied to a dependent name"* Unlike SFINAE, `requires` *can* disable a non-template member function of a template class. It does work if you remove the explicit instantiation and try to call the function. – HolyBlackCat Jun 30 '20 at 15:41
  • @HolyBlackCat I just noticed that that is indeed the case ([DEMO](https://wandbox.org/permlink/7wYi3urg2FvIzkWV)), but was unsure whether this was actually valid or is violating ODR? I guess, as per your comment, this is a different between constraint expressions and meta-programming SFINAE? Do you know which parts (possibly [\[temp.const\]](https://eel.is/c++draft/temp.constr)) that governs this? – dfrib Jun 30 '20 at 15:47
  • @HolyBlackCat From your comment and my answer above (first part), it would seems as 1) explicit instantiation definitions of non-template member functions of a class template will not respect the requires clause, whereas 2) if they are implicitly instantiated, as per [temp.inst]/4, the constraint expression will be respected when deciding which member overload that is to be instantiated (on the on-need basis)? – dfrib Jun 30 '20 at 15:50
  • I didn't look into this feature too much, but your second comment sounds correct. – HolyBlackCat Jun 30 '20 at 15:53
  • @dfri It is perfectly valid. There are plenty of such uses in the standard. Have a look at the range adaptors. Concepts are much better than sfinae. – metalfox Jun 30 '20 at 15:53
  • See, for example, [[iota.view]](https://eel.is/c++draft/range.iota.view) `end`. Nevertheless, I believe that the rest of your answer is right, modulo the last paragraph. – metalfox Jun 30 '20 at 15:58
  • @metalfox I entirely removed the SFINAE part and just added my reflections on the differences explicit/implicit instantiation; I'm still not sure what governs the correctness of the implicit case, but hopefully someone can chime in with a relevant comment or edit. – dfrib Jun 30 '20 at 15:59
  • I think that what makes my code invalid is [temp.explicit]/10, which seems to imply that all constraints must be satisfied. Explicit template instantiation seems to involve no partial ordering of constraints, but the standard wording is extremelly hard for me to understand. – metalfox Jun 30 '20 at 16:08
  • 1
    @metalfox Yes and now we also have [\[temp.explicit\]/11](http://eel.is/c++draft/temp.explicit#11) from [AndyG's answer](https://stackoverflow.com/a/62661723/4573247) that would motivate the opposite of our arguments. I fully agree that the standard wording is tricky on this one. – dfrib Jun 30 '20 at 16:20
  • Could it be that the essential difference here is actually [\[temp.constr.constr\]/2-sentence-3](http://eel.is/c++draft/temp.constr.constr#2.sentence-3)? _"Overload resolution requires the satisfaction of constraints on functions and function templates"_ – dfrib Jun 30 '20 at 16:26
1

This is a bug of the c++20 experimental implementation of both GCC and Clang.

This is reported as GCC bug #77595.

In the c++20 paragraph [temp.explicit]/11:

An explicit instantiation that names a class template specialization is also an explicit instantiation of the same kind (declaration or definition) of each of its members (not including members inherited from base classes and members that are templates) that has not been previously explicitly specialized in the translation unit containing the explicit instantiation, provided that the associated constraints, if any, of that member are satisfied by the template arguments of the explicit instantiation ([temp.constr.decl], [temp.constr.constr]), except as described below. [...]

According to this c++20 adendum "provided that the associated constraints, if any, of that member are satisfied" means that only one of the two Dot overload shall be explicitly instantiated.

The bolded clause "except as described below" was there in the c++17 standard. It does apply to the first clause "An explicit instantiation that names a class template specialization is also an explicit instantiation of the same kind (declaration or definition) of each of its members". The interpretation of this exception has not changed with c++20. Probably that the commitee overlooked that, grammaticaly, it could be correct to apply the exception to the c++20 adendum.

Below is the c++17 version of this paragraph:

An explicit instantiation that names a class template specialization is also an explicit instantiation of the same kind (declaration or definition) of each of its members (not including members inherited from base classes and members that are templates) that has not been previously explicitly specialized in the translation unit containing the explicit instantiation, except as described below.

In this shorter sentence the meaning is clear.

Oliv
  • 17,610
  • 1
  • 29
  • 72
  • Very convincing. That "except as described below" is indeed confusing, but if you know it was already there... It is curious though that this bug wasn't looked at. A. Sutton and the GCC team fixed most of the concept bugs for the GCC 10 release. – metalfox Jul 01 '20 at 14:31
  • 1
    A patch fixing it for GCC 10.3 has been accepted: https://gcc.gnu.org/pipermail/gcc-patches/2020-July/550980.html – metalfox Jul 30 '20 at 06:03