Capturing this and forwarding the lambda to another lambda in conjunction with fmt::join

135 Views Asked by At

I previously asked this question. Now I have extended it to an actual use case shown in the following example (with link here):

#include <ranges>
#include <map>
#include <functional>
#include <fmt/format.h>
#include <iostream>

template<typename RANGE, typename LAM>
auto foo(RANGE&& range, LAM&& lambda) {
    return fmt::join(range | std::views::transform( 
                [lam = std::forward<LAM>(lambda)](const auto& element) { return lam(element); }), ",");
}


class boo {
public:
    int add {2};
    
    void call_foo(){
        std::vector<int> d { 2,3,4,5,6};
        auto f = fmt::format("{}", foo(d, [this](int i) { 
            std::cout << add << std::endl;
            return add + i; 
            }));
        std::cout << f << std::endl;
    };
};

int main() {
    std::cout << "HELLO" << std::endl;
    boo b;
    b.call_foo();
}

This again results in a segfault at std::cout << add << std::endl;. Since it works without the fmt::join inside foo, I am suspecting that fmt::join does not cope well with what I got. Any ideas to what the problem is and how it can be resolved

1

There are 1 best solutions below

0
Barry On BEST ANSWER

That's because this is broken:

template<typename RANGE, typename LAM>
auto foo(RANGE&& range, LAM&& lambda) {
    return fmt::join(range | std::views::transform( 
                [lam = std::forward<LAM>(lambda)](const auto& element) { return lam(element); }), ",");
}

C++20 (and range-v3 before it) has this generalized concept of view, which isn't just an iterator/sentinel pair. It can also have arbitrary state. For views::transform, that state includes the function object (and of course the underlying view).

The problem is, fmt::join - while it looks like the other range machinery, really isn't. It just does this:

template <typename Range>
auto join(Range&& range, string_view sep)
    -> join_view<decltype(detail::adl::adlbegin(range)),
                 decltype(detail::adl::adlend(range))> {
  return join(detail::adl::adlbegin(range), detail::adl::adlend(range), sep);
}

That is - it's just pulling out begin() and end() from the range and storing those, throwing away the rest of the range.

This is okay for some ranges (notably, borrowed ranges), but not all. A views::transform with a stateful transformation (as you have here) is not a borrowed range - you need to hold onto that callable somewhere, and this isn't happening here. Hence the problems: we're trying to invoke a function object that has already been destroyed (actually one level past that - we're trying to invoke a function object that is a member of another object that has already been destroyed, through a [dangling] pointer to that parent object).

Notably, this is only a problem because the views::transform actually is destroyed before it is used. If you used fmt::join inline with the full pipeline construction, it'd work fine:

auto f = fmt::format("{}", fmt::join(d | std::views::transform([this](int i) { 
    std::cout << add << std::endl;
    return add + i; 
    }), ","));

Because the temporary transform isn't destroyed until the end of the full-expression, which is after all the formatting happens.

In general, it's best to only use fmt::join like this. And ideally we simply extend the range formatting specifiers to include a delimiter so that we just obviate the need for fmt::join entirely.