9

Consider this:

template<typename T>
struct base_t
{
    auto& f(int x) { return (T&)*this; }
    auto& f(char x) { return (T&)*this; }
};

struct derived_t : base_t<derived_t>
{

};

void test()
{
    derived_t instance;

    auto lambda = [&](derived_t&(derived_t::*g)(char))
    {
        (instance.*g)('a');

        //instance.f('a');
    };

    lambda(&derived_t::f);
}

Without commenting in that particular line ( //instance.f('a'); ) , I get the following error (MSVC 2019):

error C2664: 'void test::<lambda_1>::operator ()(derived_t &(__cdecl derived_t::* )(char)) const': cannot convert argument 1 from 'overloaded-function' to 'derived_t &(__cdecl derived_t::* )(char)'

When the line isn't commented out, it compiles just fine.

Why does referencing f inside lambda magically allow the compiler to convert this overloaded function?

Furthermore, without CRTP, this doesn't happen at all.

Edit: Additionally, as pointed out by @Jarod42,

  • Being explicit on return type auto& -> T& solves the issue.

  • If you use a named function instead of a lambda, the problem disappears. So apparently the interaction of lambdas and templates is relevant.

Hi - I love SO
  • 615
  • 3
  • 14
  • 2
    Clang accepts both, gcc acts as msvc... [Demo](https://godbolt.org/z/b889Tq). – Jarod42 Aug 15 '20 at 19:44
  • 1
    Being explicit on return type `auto&` -> `T&` [Demo](https://godbolt.org/z/3f9v9T) solves issue for gcc and msvc. – Jarod42 Aug 15 '20 at 19:53
  • Does `return *static_cast(this);` affect the outcome? – Eljay Aug 15 '20 at 20:00
  • members of template are instantiated only when *required*, so explicit call of `f` instantiate it, if you use the other overload, gcc/msvc still fail [Demo](https://godbolt.org/z/jj9cbT). That would explain gcc/msvc behavior. – Jarod42 Aug 15 '20 at 20:00
  • Asking for reference should instantiate it, but not sure how `auto` interact with that (we might see `auto` as template function)... – Jarod42 Aug 15 '20 at 20:03
  • @Eljay: I provide demo link similar to OP code, if you want to play with variation. – Jarod42 Aug 15 '20 at 20:05
  • Based on a few pages from cppreference.com and some experiments, I am currently inclined to think this is a compiler bug, but there are some details on which I would defer to a [tag:language-lawyer] working from the actual C++ specs (instead of the distilled version in cppreference.com). If it is a compiler bug, your question asks why the bug does not manifest with a certain line present, which would mean delving into the internals of the compilers. Is this what you intended to ask? Maybe you want to primarily ask if the compiler is correct in issuing this error, and why? – JaMiT Aug 23 '20 at 04:57
  • 1
    @JaMiT Well, I have no idea whether it is a bug or not, or what it even is at all. – Hi - I love SO Aug 23 '20 at 08:18
  • @Hi-IloveSO I don't have an answer, just suggestions. You can, of course, choose how you want to proceed. Sometimes bounties are a useful way to attract good answers to your question. Sometimes reformulating the question from another perspective works better. – JaMiT Aug 23 '20 at 21:41
  • I forgot to add an observation to the mix: if you use a named function instead of a lambda, the problem disappears. So apparently the interaction of lambdas and templates is relevant. – JaMiT Aug 23 '20 at 21:45
  • My guess is that this is [temp.point], and that your program is ill-formed, no diagnostic (NDR) required, due to specifically [\[temp.point\]/8](https://timsong-cpp.github.io/cppwp/n4659/temp.point#8): _"[...] If two different points of instantiation give a template specialization different meanings according to the one-definition rule, the program is ill-formed, no diagnostic required."_. ... – dfrib Aug 25 '20 at 14:07
  • ... [CWG issue 993](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#993) added that the end of the translation unit is a point of instantiation so that implementers could make the quite straightforward choice of always deferring instantiations to the end of the TU’s, and not having to bother (no diagnostic required) about whether the program is actually ill-formed due to an uncareful developer. My experience is that different compilers implement their points of instantiations differently, often being the root cause of differing behaviour in ill-formed NDR programs. – dfrib Aug 25 '20 at 14:09

1 Answers1

3

The template mechanism instantiates classes and functions as they are used. The same mechanism is used to evaluate the types behind the keyword auto.

In your case, the return types of your base_t<T>::f functions are auto&, and require a function call to be computed. Therefore when you comment out your only call to it (instance.f('a');) the actual signature of the function cannot be computed, and the compiler cannot tell whether it can be converted to derived_t&(derived_t::*g)(char).

Commenting out instance.f('a'); is possible if you define base_t<T>::f functions as follows:

template<typename T>
struct base_t
{
    T& f(int) { return *static_cast<T*>(this); }
    T& f(char) { return *static_cast<T*>(this); }
};

Here types are deduced at instantiation of the specialized type base_t<derived_t> instead of at the call of the f functions, so the compiler can figure out its conversion to the function type derived_t&(derived_t::*g)(char) without having to call them in your code.

Victor Paléologue
  • 2,025
  • 1
  • 17
  • 27
  • I thought computation of the return types would require instantiation, not specifically a function call. There is [an example of cppreference.com](https://en.cppreference.com/w/cpp/language/function#Return_type_deduction) showing that taking the address of a function template causes return types to be computed. This is not a perfect match to `&derived_t::f`, though; which of the differences removes the requirement to compute the return type? – JaMiT Aug 25 '20 at 00:35
  • Their closest example is `template auto g(T t) { return t; }`. There, it is the function that is templated, so it necessarily requires an explicit reference, such as a call, to be instantiated. In this question, it is the class that is instantiated. I just unrolled what I knew about template deduction to come to my conclusion, but I do not know what the standard really says about this case. It looks like GCC and MSVC embraced that naive approach, whereas Clang did something smarter. Both are ok until the standard specifies this case. – Victor Paléologue Aug 25 '20 at 07:52
  • If you comment both lines in the lambda and add some dependent name garbage in `f` (e.g. `T t; t.i;`), you can see that clang complains about the non-existing member `i`. Thus, at line `lambda(&derived_t::f);`, it seems to decide to instantiate all `f` overloads, while the other compilers are still complaining about overload resolution... – nop666 Aug 25 '20 at 11:14
  • @VictorPaléologue The cppreference example I was looking at is one that takes the address of a function template, specifically `void g() { int (*p)(int*) = &f; } // instantiates both fs to determine return types` (where the `f`s are function templates specifying `auto` as their return types). This code triggers template instantiation without needing a function call. – JaMiT Aug 27 '20 at 03:20