Passing overloaded CRTP class member method to lambda
Asked Answered
D

1

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.

Deterrent answered 15/8, 2020 at 19:31 Comment(12)
Clang accepts both, gcc acts as msvc... Demo.Kathiekathleen
Being explicit on return type auto& -> T& Demo solves issue for gcc and msvc.Kathiekathleen
Does return *static_cast<T*>(this); affect the outcome?Sylph
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. That would explain gcc/msvc behavior.Kathiekathleen
Asking for reference should instantiate it, but not sure how auto interact with that (we might see auto as template function)...Kathiekathleen
@Eljay: I provide demo link similar to OP code, if you want to play with variation.Kathiekathleen
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 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?Oren
@Oren Well, I have no idea whether it is a bug or not, or what it even is at all.Deterrent
@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.Oren
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.Oren
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: "[...] 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.". ...Xerxes
... CWG issue 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.Xerxes
C
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.

Cupp answered 24/8, 2020 at 16:25 Comment(4)
I thought computation of the return types would require instantiation, not specifically a function call. There is an example of cppreference.com 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?Oren
Their closest example is template<typename T> 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.Monotone
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...Logion
@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 fs are function templates specifying auto as their return types). This code triggers template instantiation without needing a function call.Oren

© 2022 - 2024 — McMap. All rights reserved.