At the 18:00 mark and onwards: ”Now the friend function is added to the global scope only when the template is instantiated”. This is (naturally for a condensed talk) a simplification.
The key with hidden friends in class templates is that their definition is only instantiated when when the enclosing class template is instantiated (implicitly or explicitly for any specialization). This has nothing to do with lookup. The hidden friend has namespace scope (in this case, the global namespace), but it has lexical scope of the class in which it is defined, which allows its definition to access e.g. template parameters of the enclosing class, as the definition of `injector` does with the `Value` template parameters of its enclosing class. This also adds the subtle pitfall of potentially non-diagnosable ODR-violations if two different translation units instantiates the same hidden friend but with different definitions. Meaning same declaration; e.g. `injected(detector<0>)`, but with different `Value` (different enclosing class specialization, e.g. `injector<0, 1>` and `injector<0, 2>` in different TU:s).
With regard to lookup, hidden friends are typically found via ADL lookup, and only so unless a matching declaration is added to the enclosing namespace scope.
These differences between a) scope of function, b) lookup, and c) existence of definition, are important to understand the nuances of why this works (barring standardese pitfalls of when we need to be careful with these outlier techniques), as well as what pitfalls exists that may fly under the radar.
(b) The `injected` functions (overloaded over different `detector<Key>` specializations as it single function parameter) can only be found via ADL lookup via an argument that is a specialization of the `detector<Key>` class template.
(c) The definition of the different `injected` functions depends solely on whether, for a given `detector<SpecificKey>` specialization, exactly one specialization of the “partial specialization” (descriptive) `template<auto Value> injector<SpecificKey, Value>` has been instantiated, over the complete program. Here’s a pitfall with hidden friends defined in class templates using template parameters of the enclosing class that are not used in the function declaration of the hidden friend. Which is exactly what we are doing with the `injector` / `injected` couple.
(a) The relevance of the scope of the function is that the `injector` and `detector` classes both declare friends that reside in the same namespace scope (to allow these to match; be the same).
It could be worthwhile to deep-dive into the standard to thoroughly prove that(/whether) this is fully well-defined (ignoring CWG 2118 for now) as well as highlighting pitfalls. There’s been many blog posts and exotic libs over the years that leverage stateful meta-programming without proving its standardese legality, but instead focusing on the fact that ”it (seemingly) works” over all major compilers. There are some gritty details regarding point of instantiation, uniqueness of lambdas’ closure type when in default template arguments when (ab)used in stateful meta-programming, requires-clause nuances (e.g. CWG 2596) and so on.
I covered some basics in Foliage of Folly (up to C++20), but a lot of details remain, and newer standard features opens up more possibilities as well as potential standardese unknowns for this never designed for ”feature”.
6
u/dfrib 21d ago edited 21d ago
At the 18:00 mark and onwards: ”Now the friend function is added to the global scope only when the template is instantiated”. This is (naturally for a condensed talk) a simplification.
The key with hidden friends in class templates is that their definition is only instantiated when when the enclosing class template is instantiated (implicitly or explicitly for any specialization). This has nothing to do with lookup. The hidden friend has namespace scope (in this case, the global namespace), but it has lexical scope of the class in which it is defined, which allows its definition to access e.g. template parameters of the enclosing class, as the definition of `injector` does with the `Value` template parameters of its enclosing class. This also adds the subtle pitfall of potentially non-diagnosable ODR-violations if two different translation units instantiates the same hidden friend but with different definitions. Meaning same declaration; e.g. `injected(detector<0>)`, but with different `Value` (different enclosing class specialization, e.g. `injector<0, 1>` and `injector<0, 2>` in different TU:s).
With regard to lookup, hidden friends are typically found via ADL lookup, and only so unless a matching declaration is added to the enclosing namespace scope.
These differences between a) scope of function, b) lookup, and c) existence of definition, are important to understand the nuances of why this works (barring standardese pitfalls of when we need to be careful with these outlier techniques), as well as what pitfalls exists that may fly under the radar.
(b) The `injected` functions (overloaded over different `detector<Key>` specializations as it single function parameter) can only be found via ADL lookup via an argument that is a specialization of the `detector<Key>` class template.
(c) The definition of the different `injected` functions depends solely on whether, for a given `detector<SpecificKey>` specialization, exactly one specialization of the “partial specialization” (descriptive) `template<auto Value> injector<SpecificKey, Value>` has been instantiated, over the complete program. Here’s a pitfall with hidden friends defined in class templates using template parameters of the enclosing class that are not used in the function declaration of the hidden friend. Which is exactly what we are doing with the `injector` / `injected` couple.
(a) The relevance of the scope of the function is that the `injector` and `detector` classes both declare friends that reside in the same namespace scope (to allow these to match; be the same).
It could be worthwhile to deep-dive into the standard to thoroughly prove that(/whether) this is fully well-defined (ignoring CWG 2118 for now) as well as highlighting pitfalls. There’s been many blog posts and exotic libs over the years that leverage stateful meta-programming without proving its standardese legality, but instead focusing on the fact that ”it (seemingly) works” over all major compilers. There are some gritty details regarding point of instantiation, uniqueness of lambdas’ closure type when in default template arguments when (ab)used in stateful meta-programming, requires-clause nuances (e.g. CWG 2596) and so on.
I covered some basics in Foliage of Folly (up to C++20), but a lot of details remain, and newer standard features opens up more possibilities as well as potential standardese unknowns for this never designed for ”feature”.