1

Explanation:

CLion and it's standard compiler give me an error that the "candidate template [is] ignored", when I write a lambda as parameter for a generic function that takes a lambda as argument. This lambda takes a generic type T and returns another unknown type A.

The container class that I am writing is supposed to support functional operations like these in Scala or the ones from the Java Stream API.

To be exact: The map function makes huge problems. It is implemented as a member function in a class called Sequence, which takes a generic parameter T. It is supposed to take an element of an already known type T ( actually it iterates through the whole sequence ) and convert it into an unknown type A. The implementation itself is not the problem, but I cannot call the function with the lambda syntax I know.

Code:

Sequence.h

template< typename T >
class Sequence {
public:
    template< typename A >
    auto map( std::function< A( const T ) > function ) const {
        auto sequence = new Sequence< A >;
        for ( const T& element : *this ) {
            sequence->push( function( element ) );
        }
        return *sequence;
    }
}

main.cpp

int main() {
    Sequence< uint32_t > a;
    a.push( 20 );
    a.push( 30 );
    a.push( 40 );

    a.map( []( uint32_t c ) -> uint32_t {
        return c * c;
    } );
    return 0;
}

As far as I understand a lambda gets initialized, which takes a parameter of type std::uint32_t and returns a value of type std::uint32_t. The generic parameter A doesn't seem to get inferred at this point.

Error Stack:

main.cpp:21:7: error: no matching function for call to 'Sequence<unsigned int>::map(main()::<lambda(uint32_t)>)'
     } );

Sequence.h:143:10: note: candidate: template<class A> auto Sequence<T>::map(std::function<A(T)>) const [with A = A; T = unsigned int]
     auto map( std::function< A( const T ) > function ) const {

note:   template argument deduction/substitution failed:
main.cpp:21:7: note:   'main()::<lambda(uint32_t)>' is not derived from 'std::function<A(unsigned int)>'
     } );

Thanks in advance!

max66
  • 65,235
  • 10
  • 71
  • 111
llambdaa
  • 65
  • 6
  • You need to replace `a.map(` with `a.map(` since it's not possible to deduce the type here. The proper solution IMO is to not use `std::function` here. Use a forwarding reference instead (`template auto map(F && func)`. – Nikos C. Apr 21 '19 at 16:05

2 Answers2

2

Ignoring the const problem, you have a sort of chicken-and-egg problem.

It's true that your lambda can be converted to a std::function<std::uint32_t(std::unit32_t)>.

But it's also true that the lambda isn't a std::function<std::uint32_t(std::unit32_t)> so the compiler can't deduce A.

And if the compiler can't deduce A, can't convert the lambda to std::function<A(T)>.

You obviously can explicit the correct std::function type calling map()

a.map(std::function<std::uint32_t(std::uint32_t)>{[]( uint32_t c ) -> uint32_t {
    return c * c;
}});

and, taking in count that you're using C++17 (so you can use the deduction guides for std::function) also deducing the template parameters for std::function

a.map(std::function{[]( uint32_t c ) -> uint32_t {
    return c * c;
}});

but, using again the template deduction guides for std::function, what about writing mat() to accept a simply callable and deducing A from it?

I mean... what about something as follows?

template <typename F>
auto map( F && func ) const {
    using A = typename decltype(std::function{std::forward<F>(func)})::result_type;

    auto sequence = new Sequence< A >;
    for ( const T& element : *this ) {
        sequence->push( std::forward<F>(func)( element ) );
    }
    return *sequence;
}

(caution: code not tested).

You can also deduce A, without std::function deduction guides (so before C++17), as suggested by Michael Kenzel.

max66
  • 65,235
  • 10
  • 71
  • 111
  • Well, but how do I get the compiler to properly deduce in this situation? I think there is a way I don't know since this is possible in other languages too. – llambdaa Apr 21 '19 at 15:59
  • @Lyras - answer improved; hope this helps (but caution that I haven't tested the code). – max66 Apr 21 '19 at 16:03
  • @Lyras - `typename` forgotten in last example; added; sorry. – max66 Apr 21 '19 at 16:07
1

As already pointed out by @cpplearner in the comments above, the reason why this won't work is explained in detail here: Constructing std::function argument from lambda.

If your Sequence is already a template, there should be no reason to require the callable for your map function to be passed in the form of an std::function. At least, I seem unable to come up with a reason that could justify doing this. std::function is generally not free to construct or call, and also can inhibit inlining. Best avoid it unless you really need the capability to store arbitrary callables for later use. Simply take a forwarding reference, e.g.:

template <typename F>
auto map(F&& f) const;

You should be able to deduce the result type of whatever invoking f on an element of your sequence ends up producing, e.g., via

decltype(f(std::declval<T>()))

Furthermore, don't just return a raw pointer to a new Sequence. Use an std::unique_ptr.

Putting it all together, your map function could look something like this:

template <typename F>
auto map(F&& f) const
{
    using output_element_type = decltype(f(std::declval<T>()));

    auto sequence = std::make_unique<Sequence<output_element_type>>();

    for (const T& element : *this)
        sequence->push(f(element));

    return sequence;
}
Michael Kenzel
  • 15,508
  • 2
  • 30
  • 39
  • Maybe using `std::forward(f)` instead of simply `f` ? – max66 Apr 21 '19 at 16:09
  • @Max66 since I'm not passing on the callable `f` to anywhere, I don't really see what applying `std::forward(f)` would achieve here!? – Michael Kenzel Apr 21 '19 at 16:11
  • I admit that I have big troubles understanding how exactly works perfect forwarding; anyway, [this answer](https://stackoverflow.com/a/36920686/6022656) show that isn't exactly the same thing with or without `std::forward` – max66 Apr 21 '19 at 16:18
  • @max66 `std::forward` is used when you want to pass-on the forwarding reference. Here, it's not passed on. It's just called. If the code was `push(f)`, then yes, it would be better to use `push(std::forward(f))`. But the code is `push(f(element))`, which means the function is just called and not passed. The value returned by `f` is passed, not `f` itself. – Nikos C. Apr 21 '19 at 16:22
  • @max66 Yes, a ref-qualified call operator is the one situation where there would be a difference. I would argue, however, that using `std::forward` when invoking a callable multiple times (like we do here) is semantically invalid and potentially dangerous. The defining characteristic of an rvalue is that it cannot explicitly be referred to. I believe, there is no way the same rvalue could ever be used twice in an expression. Thus, it would arguably be valid for an `&&` qualified call operator to do something that can only be done once. Thus, forwarding `f` into every invocation may invoke UB… – Michael Kenzel Apr 21 '19 at 17:34
  • @MichaelKenzel - Uhmmmm...I see the risks of a multiple `std::forward` invocation but I don't think it's correct count as invocation the one inside a `decltype()`. – max66 Apr 21 '19 at 19:41
  • @max66 The one inside `decltype()` has to be the same as the actual call since a potential rvalue-qualified call operator may have a different return type… – Michael Kenzel Apr 21 '19 at 19:42
  • @MichaelKenzel - But the one inside the `decltype()` isn't a real call; it's only used by the compiler to detect a type. – max66 Apr 21 '19 at 19:47
  • @max66 Yes, but we want it to detect the type that the actual call will return, not the type that a potentially different `operator ()` would return!? – Michael Kenzel Apr 21 '19 at 19:48
  • @MichaelKenzel - uhmmm... in this case we want detect the type returned by the same call (`const` plus or minus) that we do after; so if the real call is with `std::forward`, the call inside `decltype()` should be with `std::forward`; if the real call is without `std::forward`, the call inside `decltype()` should be without. I suppose. – max66 Apr 21 '19 at 19:52
  • @max66 Exactly. And, as described above, I would argue that using `std::forward` here is not a good idea because the callable will be invoked multiple times… – Michael Kenzel Apr 21 '19 at 20:09
  • @MichaelKenzel - Anyway... not conclusive but in [`std::forward` page in cpp reference](https://en.cppreference.com/w/cpp/utility/forward) you can see an example (the second `wrapper` box) of "double forward" of the same object (one inside `decltype()` and one outside). Not exactly our case but not so different. – max66 Apr 21 '19 at 20:43
  • @MichaelKenzel - Sorry but I'm puzzled. I've added [a question](https://stackoverflow.com/questions/55786775/how-do-correctly-use-a-callable-passed-through-forwarding-reference) but I'm not sure to understand correctly what do you sustain. Feel free to modify the question to better explain what do you mean. – max66 Apr 21 '19 at 21:14
  • @max66 I guess I understand our misunderstanding now. I was not trying to argue that the call in the expression of the `decltype` specifier itself it a problem. The problem here is that the callable is going to be called in the loop that loops over the container and, thus, invoked multiple times. My argument was that, therefore, using `std::forward` in the call inside the loop is potentially problematic. The expression in the `decltype` simply must match whatever it is that we're doing in the loop since we're interested in the type of the result of the call that happens in the loop… – Michael Kenzel Apr 21 '19 at 21:29
  • @MichaelKenzel - well... about `decltype()` I'm convinced that isn't a problem; but about multiple calls (inside a loop like the `bar()` function in my question) I'm very doubtful. Please, feel free to correct my question if you want. – max66 Apr 21 '19 at 21:34
  • @max66 I'm also not 100% sure. I'll present my argument as an answer there, I hope others will be able to either confirm or show me where my argument is wrong. I'm genuinely interested in an answer to this, in fact, I put it on my list of future questions to ask just before. ;) I would just suggest that you put a link to the original thread here into your question there. – Michael Kenzel Apr 21 '19 at 21:38