1

So I have been looking into using abstracted algorithms to reuse reoccurring patterns in my code. Specifically, I want to determine the the element in an array of nodes that has the highest 'score', determined by evaluation of a complex scoring member function.

After some help I've come up with (C++17)

template <typename FwdIt, typename Eval, typename Pred = std::less<>>
constexpr FwdIt max_eval_element(FwdIt first, FwdIt last, Eval eval, Pred pred = Pred()) {
    FwdIt found = first;
    if (first != last) {
        auto best = eval(*found);
        while (++first != last) {
            if (auto const thisVal = eval(*first);
                pred(best, thisVal)) {
                found = first;
                best = thisVal;
            }
        }
    }
    return found;
}

So consider my Node class:

class Node {
private:
    double val;
public:
    Node(double val) noexcept : val(val) {}

    [[nodiscard]] auto Score1() const noexcept {
        return std::sqrt(std::log(10.0 / val));
    }

    [[nodiscard]] auto Score2(double other) const noexcept {
        return std::sqrt(std::log(other / val));
    }
};

and my array of nodes:

std::array<Node, 100000> nodes;

I can call

auto const& Node = *std::max_eval_element(std::cbegin(nodes), std::cend(nodes), std::mem_fn(&Node::Score1));

but now I want to repeat this for Score2, where the input depends on some local variable... of course I can write some lambda function... but we have std::bind for that, right? I know you can cal bind on a member function like

std::bind(this, std::mem_fn(Node::Score1));

But what I want is the other way around. Which doesn't work.

auto const& Node = *std::max_eval_element(std::cbegin(nodes), std::cend(nodes), std::bind(std::mem_fn(&Node::Score2), 1.0));

I tried it the other way around, but that also doesn't work

auto const& Node = *std::max_eval_element(std::cbegin(nodes), std::cend(nodes), std::mem_fn(std::bind(&Node::Score2), 1.0));

I know why this doesn't work... a member function requires the object pointer as a (hidden) first argument. But that would mean we are missing something like std::bind_mem_fn or so... We had std::bind2nd in the past, but it has been removed in C++17...

Again: of course I can use a lambda, but considering things like std:mem_fn and std::bind exist, and abstracted algorithms is a good thing...

Am I missing something, Or is this just missing from the standard?

JHBonarius
  • 10,824
  • 3
  • 22
  • 41
  • 4
    "I can write some lambda function... but we have std::bind for that, right?" -> that's the other way around; you should really prefer lambda's because they raise a lot less questions than using `std::bind` does. – rubenvb Oct 24 '19 at 10:09
  • @rubenvb I think opinions differ on that... also: DRY... if all lambda's look the same, abstraction would be preferred (opinionated) – JHBonarius Oct 24 '19 at 10:10
  • 3
    nothing prevents you from not repeating yourself with lambda's. You don't have to declare them inline for starters. – rubenvb Oct 24 '19 at 10:11
  • 2
    and lambdas tend to be optimized much better by the compilers... – bartop Oct 24 '19 at 10:13
  • @bartop do they? better then memfn and bind? you have proof? – JHBonarius Oct 24 '19 at 10:14
  • 1
    Isn't this what `std::placeholders` is for? `std::bind(std::mem_fn(&Node::Score2), std::placeholders::_1, 1.0)` https://godbolt.org/z/v_U40a – Mark Oct 24 '19 at 10:17
  • 1
    Also, your `max_eval_element` does not handle empty ranges properly. The `auto best = eval(*found);` should be in the `if`. – Mark Oct 24 '19 at 10:20
  • AFAIK "Effective Modern C++" by Scott Meyers – bartop Oct 24 '19 at 10:22
  • @JHBonarius using lambdas does not necessarily imply to violate DRY. You can eg write a function that returns the lambda and reuse it whereever you like. – 463035818_is_not_an_ai Oct 24 '19 at 10:22
  • The bind expression is much less readable. – L. F. Oct 24 '19 at 10:26
  • 1
    @JHBonarius both lambdas and bind are prvalue expressions of some object type you don't want to (or can't) name. There is no difference in DRYness between their use – Caleth Oct 24 '19 at 10:30
  • @bartop , hmm maybe you're right there... I tried some code [here](https://godbolt.org/z/zDr5CS). If you look at the lambda version, it seemed the division was inlined, while the mem_fn results in a function call... weird... I wouldn't expect this. Thanks for the insight – JHBonarius Oct 24 '19 at 10:43
  • 3
    Lambdas are way more flexible and idiomatic than `std::bind`. They amount to (more or less) syntactic sugar for functor objects, so the compiler is guaranteed to have all the information it could use for optimization right at its fingertips. `std::bind` comes from a time before lamdbas existed and has been superseded by them. – Max Langhof Oct 24 '19 at 10:44

2 Answers2

3

The call std::bind(&Node::Score2) is the problem. It lacks arguments to pass to Score2. You want:

std::bind(&Node::Score2, std::placeholders::_1, 1.0)

This is not a pointer-to-member, so it is not an appropriate argument to std::mem_fn

Alternatively, you can use a lambda

[](const auto & node) { return node.Score2(1.0); }
Caleth
  • 52,200
  • 2
  • 44
  • 75
  • Noice! So `std::mem_fn(&someFn)` is effectively equal to `std::bind(&someFn, std::placeholders::_1)`? – JHBonarius Oct 24 '19 at 10:32
  • haha, true... is not equal then... It would be an accident waiting to happen – JHBonarius Oct 24 '19 at 10:36
  • infact, no, it's akin to `std::bind(&SomeClass::someFn, std::placeholders::_1, ... std::placeholders::_N)`, i.e. it always has exactly the right number of parameters – Caleth Oct 24 '19 at 10:39
0

I've come to the conclusion that lambda's optimize better than std::mem_fn

Take this code:

#include <iostream>
#include <array>
#include <functional>

class Node {
private:
    double val;
public:
    Node(double val) noexcept : val(val) {}

    [[nodiscard]] auto Score() const noexcept {
        return 10.0 / val;
    }
};

template <typename FwdIt, typename Eval, typename Pred = std::less<>>
constexpr FwdIt max_eval_element(FwdIt first, FwdIt last, Eval eval, Pred pred = Pred()) {
    FwdIt found = first;
    if (first != last) {
        auto best = eval(*found);
        while (++first != last) {
            if (auto const thisVal = eval(*first);
                pred(best, thisVal)) {
                found = first;
                best = thisVal;
            }
        }
    }
    return found;
}

int main()
{
    std::array<Node, 10> nodes{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    auto nodeIt1 = max_eval_element(std::cbegin(nodes), std::cend(nodes), std::mem_fn(&Node::Score));
    std::cout << "dist1 " << std::distance(std::cbegin(nodes), nodeIt1) << std::endl;

    auto nodeIt2 = max_eval_element(std::cbegin(nodes), std::cend(nodes), [](Node const& node) { return node.Score(); });
    std::cout << "dist2 " << std::distance(std::cbegin(nodes), nodeIt2) << std::endl;
}

If you look at the compiled assembly (e.g. on GodBolt, with -O2 ), the std:mem_fn results is a function call

42 movsd   QWORD PTR [rsp+8], xmm1
43 call    Node::Score() const
44 movsd   xmm1, QWORD PTR [rsp+8]

, while the lambda is inlined

69 movsd   xmm1, QWORD PTR .LC0[rip]
70 divsd   xmm1, QWORD PTR [rax]
JHBonarius
  • 10,824
  • 3
  • 22
  • 41
  • 1
    No surprise. `mem_fn` invocations go through a function pointer, which are notoriously difficult to optimize away. Lambdas use a direct function call, which are a piece of cake for any garden-variety optimizer. – j6t Oct 24 '19 at 12:10
  • Bah... apparently insight and explanation thereof is good enough to get you a -1 on stack overflow. Whatever happened to "be nice"? – JHBonarius Oct 24 '19 at 12:38