1

This is a stripped down version of my program to illustrate the issue:

#include <functional>
#include <iostream>

template<class T>
class Piped {
public:
    typedef T value_type;
};

template<class P1, class P2, class T>
class PipeConnector : public Piped<T> {
public:
    PipeConnector(const P1 &p1, const P2 &p2)
     : m_1(p1), m_2(p2) { }

    bool run(const T &element) const {
        return m_1.run(element) || m_2.run(element);
    }
private:
    const P1 &m_1;
    const P2 &m_2;
};

template<class T>
class NullOp : public Piped<T> {
public:
    bool run(const T&) const {
        return false;
    }
};

template<class T, class Functor>
class FunctionOp : public Piped<T> {
public:
    FunctionOp(Functor f1)
     : m_1(f1) { }

    bool run(const T &element) const {
        return m_1(element);
    }
private:
    std::function<bool(T)> m_1;
};

template<class P1, class Functor>
auto operator|(const P1 &p1, const Functor &f2) {
    return PipeConnector<P1,
            FunctionOp<typename P1::value_type, std::function<bool(typename P1::value_type)>>, typename P1::value_type>(
                    p1, FunctionOp<typename P1::value_type, std::function<bool(typename P1::value_type)>>(f2));
}

int main() {
    auto p = NullOp<int>() | [](int x) -> bool { if (x < 10) { return true;} return false; };
    std::cout << p.run(20) << std::endl;
    return 0;
}

Compiling this program using g++ / clang++ -std=c++14 leads to a segfault. Adding -O3 to it, makes it run without a segfault.

When changing the PipeConnector to not store const references but storing copies, this works. I assume the problem is some lambda scope issue, but I don't understand what goes wrong. -O3 seems to elide the problem? Could you explain the issue to me, please?

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
psqr
  • 23
  • 7

2 Answers2

1

The problem is that NullOp<int>() is a temporary to which you store a const-reference to inside PipedConnector. This temporary has full-expression lifetime so it does not exist after p has been initialized. When you call p.run(20) you then reference that temporary again, which has since been deleted. The UB that results from that can cause a crash.

David G
  • 94,763
  • 41
  • 167
  • 253
  • That would be an explanation, but i had a look at the standard, which says, that "There are two contexts in which temporaries are destroyed at a different point than the end of the fullexpression" : "The second context is when a reference is bound to a temporary.116 The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except: .." – psqr May 26 '15 at 16:27
  • @psqr One of the exceptions is "A temporary object bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call." The temporary is bound to the reference `p1` of your `operator|`, and then it is deleted at the end of the full expression (which is the function call), leaving `PipeConnector::p1` a dangling reference. – David G May 26 '15 at 17:57
0

Your operator|() creates a PipeConnector object, both of whose arguments are temporaries. The PipeConnector constructor then returns an object containing references to these temporaries. When that object's run method is invoked, the lifetime of the temporaries has expired.

The NullOp object has no state, and it probably doesn't matter (in practice) that its reference is dangling, but the same cannot be said of the std::function object constructed from the lambda. This is not the same as the lambda itself; the lambda itself is stateless, but the std::function operator contains a function pointer.

I suppose that with -O3 the compiler can figure out what function should be invoked when the std::function member variable is called, and so it inlines the call. But it is still UB, even when it appears to work.

Edit: OP suggests that the lifetime of the temporary should be extended because it is bound to a reference. However, in this context, that does not apply. See Does a const reference prolong the life of a temporary?

There are three references initialized with temporary objects in the constructor of the PipeConnector created in the call to operator|(). The first of these, NullOp<int>(), is the argument to the operator|() function, and its lifetime is explicitly not extended by the wording of §12.2 [class.temporary] paragraph 5.1:

A temporary object bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.

The other two temporary objects are an instance of std::function<bool(int)> which results from the conversion of a lambda, used in the initializer of a FunctionOp<int, std::function<bool(int)>>, and the constructed FunctionOp itself. In versions of the draft standard available in 2013 and the first half of 2014, this case (which is very similar to the function call case) had similar wording:

A temporary bound to a reference member in a constructor's ctor-initializer (12.6.2 [class.base.init]) persists until the constructor exits.

However, that sentence was deleted in the proposed resolution to DR 1696 (committed to the draft standard 29 September 2014). I believe the sentence was deleted for clarity; as far as I can see, the situation referred is marked as invalid by the other modifications in the resolution to DR1696, especially the new paragraph 8 in §12.6.2:

A temporary expression bound to a reference member in a mem-initializer is ill-formed.

Community
  • 1
  • 1
rici
  • 234,347
  • 28
  • 237
  • 341
  • But I do copy the lambda into m_1 (in FunctionOp) by value, right? And then reference the FunctionOp instantiation which should call the std::function copy, doesn't it? – psqr May 26 '15 at 14:37
  • @psqr: That's true, but the FunctionOp which is passed to the PipedConnector is itself a temporary. In fact, both arguments to the PipeConnector constructor are temporaries. Hopefully the edit I made is more precise. – rici May 26 '15 at 16:12
  • Thanks a lot! I understand the reasoning, but had a look at the standard section 12.2.4 (see my answer to 0x499602D2) which states that temporaries have a fullexpression lifetime, except when used in two contexts. And one context is when a reference is bound to the temporary. So I would think that the temporary has to live until my `PipeConnector` instance is destructed. Or do i misinterpret the standard? – psqr May 26 '15 at 16:40
  • @psqr: I don't believe that exception applies. The reference to which the temporary is bound is the argument of the constructor, not the member variable. At least, that seems to be the way the compilers I have available implement it. (Eg. http://ideone.com/JQnPKU) – rici May 26 '15 at 17:08
  • @rici It can't be the argument of the constructor because the temporary `NullOp()` is the argument to the `operator|` function. It then becomes bound to the reference parameter so the exception in my below comment does apply, like I told @psqr. – David G May 27 '15 at 00:11
  • @0x499602D2: I believe both the `NullOp()` and the `std::function` temporaries are problematic, and both cause UB. So both need to be fixed. But in practice, the `NullOp()` temporary does not cause any problems, at least with gcc, even without optimization. The object is stateless and non-virtual, so even though the reference is dangling, the call to `m_1.run()` does not actually use the reference. (That doesn't stop it being UB, I repeat. It's still erroneous. But the segfault comes from the `std::function` temporary.) – rici May 27 '15 at 01:57