-3

To understand the problems with object slicing, I thought I have created a horrible example and I was trying to test it. However, the example is not as bad as I thought it would be.

Below is a minimal working example, and I would appreciate if you helped me understand why it is still "working properly". It would be even better if you helped me make the example worse.

#include <functional>
#include <iostream>

template <class T> class Base {
protected:
  std::function<T()> f; // inherited

public:
  Base() : f{[]() { return T{0}; }} {} // initialized
  virtual T func1() const { return f(); }
  virtual ~Base() = default; // avoid memory leak for children
};

template <class T> class Child : public Base<T> {
private:
  T val;

public:
  Child() : Child(T{0}) {}
  Child(const T &val) : Base<T>{}, val{val} { // initialize Base<T>::f
    Base<T>::f = [&]() { return this->val; }; // copy assign Base<T>::f
  }
  T func1() const override { return T{2} * Base<T>::f(); }
  void setval(const T &val) { this->val = val; }
};

template <class T> T indirect(const Base<T> b) { return b.func1(); }

int main(int argc, char *argv[]) {
  Base<double> b;
  Child<double> c{5};

  std::cout << "c.func1() (before): " << c.func1() << '\n'; // as expected
  c.setval(10);
  std::cout << "c.func1() (after): " << c.func1() << '\n'; // as expected

  std::cout << "indirect(b): " << indirect(b) << '\n'; // as expected
  std::cout << "indirect(c): " << indirect(c) << '\n'; // not as expected

  return 0;
}

The output I get when I compile the code is as follows:

c.func1() (before): 10
c.func1() (after): 20
indirect(b): 0
indirect(c): 10

I would expect the last line to throw some exception or simply fail. When the base part of c gets sliced in indirect, there is no this->val to be used inside the lambda expression (I know, C++ is a statically compiled language, not a dynamic one). I have also tried capturing this->val by value when copy assigning Base<T>::f, but it did not change the result.

Basically, my question is two folds. First, is this undefined behaviour, or simply a legal code? Second, if this is a legal code, why is the behaviour not affected by slicing? I mean, I can see that T func1() const is called from the Base<T> part, but why is the captured value not causing any trouble?

Finally, how can I build an example to have worse side-effects such as memory access type of problems?

Thank you in advance for your time.

EDIT. I am aware of the other topic that has been marked as duplicate. I have read all the posts there, and in fact, I have been trying to duplicate the last post there. As I have asked above, I am trying to get the behaviour

Then the information in b about member bar is lost in a.

which I cannot get fully. To me, only partial information seems to be lost. Basically, in the last post, the person claims

The extra information from the instance has been lost, and f is now prone to undefined behaviour.

In my example, f seems to be working just as well. Instead, I just have the call to T Base<T>::func1() const, which is no surprise.

Arda Aytekin
  • 1,231
  • 14
  • 24
  • "_and f is now prone to undefined behaviour_" "_In my example, `f` seems to be working just as well._" Undefined behavior is undefined. One of many possible manifestations of undefined behavior is code _seeming_ to work fine. – Algirdas Preidžius Oct 20 '17 at 10:09
  • @AlgirdasPreidžius, so is the behaviour above, in my example, undefined? Am I simply getting _seemingly_ working code simply because of luck or some caching of variables in this limited, simplest example? That's what I wanted to understand. – Arda Aytekin Oct 20 '17 at 10:20
  • I don't know, if the code in your example produces undefined behavior. I just wanted to state, that the fact that code _seems_ to work fine is **not** a proof that it doesn't exhibit undefined behavior. – Algirdas Preidžius Oct 20 '17 at 11:39
  • 1
    @ArdaAytekin you're capturing by reference in the lambda. What even is the purpose of the lambda? The lambda capturing by reference and being copied is an even more insidious error in my opinion. `Child d=c; c.setval(55); std::cout << d.func1();` prints `55`. Now think about what that means for `Child d; {Child c{55}; d=c;} std::cout << d.func1();` – PeterT Oct 20 '17 at 16:20
  • @ArdaAytekin I have an answer for you. Your code is dangerous, but don't have undefined behavior. As soon as this question is reopened, I'll explain in an answer. – Guillaume Racicot Oct 20 '17 at 17:20
  • @PeterT, thank you for your small but explanatory comment. I think @GuillaumeRacicot's answer below also points to the same problem there. I hadn't thought of that problem of copying the lambda that has the capture by reference property. I was simply after `this->val` getting invalidated _during_ the slice. However, my object was still in the same scope, and thus, I didn't experience any problem. Now I see. Could you please provide a better "bad example" so that the information lost during slicing results in a bad behaviour? I mean, inside `indirect` maybe? – Arda Aytekin Oct 21 '17 at 11:09
  • @AlgirdasPreidžius, thank you for your comment. I know what undefined behavior means. I wasn't showing this example as a **proof**, though. I was simply asking if this example has undefined behavior. Apparently, according to the answer below, the code was legitimate. Well, it **does** have undefined behavior when I assign things in another scope and try to reach a deallocated memory as is provided in PeterT's comment. Thank you anyways... – Arda Aytekin Oct 21 '17 at 11:11

1 Answers1

1

There is no undefined behavior with your current code. However, it's dangerous and therefore easy to make undefined behavior with it.

The slicing happen, and yet you access this->val. Seems like magic, but you're just accessing the this->val from Child<double> c from your main!

That's because of the lambda capture. You capture this, which points to your c variable in your main. You then assign that lambda into a std::function inside your base class. You base class now have a pointer to the c variable, and a way to access the val through the std::function.

So the slicing occurs, but you access to the unsliced object.

This is also why the number is not multiplied by two. The virtual call resolves to base, and the value of val in c in your main is 10.

Your code is roughly equivalent to that:

struct B;

struct A {
    B* b = nullptr;

    int func1() const;
};

struct B : A {
    int val;
    explicit B(int v) : A{this}, val{v} {}
};

int A::func1() const {
    return b->val;
}

int main() {
    B b{10};

    A a = b;

    std::cout << a.func1() << std::endl;
}
Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • Thank you for both being kind enough to answer as opposed to simply claiming that this post is duplicate and being very pedagogic in your explanation. Now I can also see how dangerous things can get in this code, especially when the `B` (or, `Child`) instance leaves scope and `this` gets invalidated in the `Base` slice of `Child`. This might have been the behavior in https://stackoverflow.com/a/46725480/4720025, which I was trying to understand at first place. Could you also please elaborate a bit on capturing by value, too? I hadn't thought that I was capturing `this` but just `this->val`. – Arda Aytekin Oct 21 '17 at 11:02
  • @ArdaAytekin if you have c++14, you can capture like that: `val = this->val`. It will capture `val` by value. Note that if the value of value change in `Child`, the value in the lambda won't be updated. – Guillaume Racicot Oct 21 '17 at 14:13
  • why do I need C++14 for that? I mean, isn't capture by value supported in C++11? Which specific feature in C++11 enables this type of behavior? – Arda Aytekin Oct 21 '17 at 14:38
  • @ArdaAytekin it is supported, but harder to do in case of members. You see, to capture member, the lambda must capture`this`, but `this` is a pointer! So to capture a member by value in C++11, you must create a local variable equal to your member and capture that local variable. C++14 simplify this process by allowing `= value` after a capture to set a custom value. – Guillaume Racicot Oct 21 '17 at 14:42
  • @ArdaAytekin read more about it at the [cppreference page about lambda](http://en.cppreference.com/w/cpp/language/lambda) – Guillaume Racicot Oct 21 '17 at 14:44
  • @ArdaAytekin no problem! But next time, be careful when posting a question that talk about a well known problem. Be very clear that it's a different problem than the already answered one. We get so spammed by slicing questions and other stuff beginners get caught in, some false positive like your question sadly get closed and downvoted. – Guillaume Racicot Oct 21 '17 at 16:51
  • thank you for the nice tip. And, after reading about captures in cppreference, I have understood what you meant by C++14. According to `When a lambda captures a member using implicit by-copy capture, it does not make a copy of that member variable: the use of a member variable m is treated as an expression (*this).m, and *this is always implicitly captured by reference:`, I would have had dangling pointer problem again, even if I had used capture by copy inside the struct naively --- that is, without assigning it to a local variable first. – Arda Aytekin Oct 21 '17 at 17:07