36

I was searching a bug in an application, which I've finally fixed but didn't understand completely. The behavior can be reproduced with the following simple program:

#include <iostream>
#include <memory>
#include <functional>

struct Foo
{
  virtual int operator()(void) { return 1; } 
};

struct Bar : public Foo
{
  virtual int operator()(void) override { return 2; }
};

int main()
{
    std::shared_ptr<Foo> p = std::make_shared<Bar>();
    std::cout << (*p)() << std::endl;

    std::function<int(void)> f;
    f = *p;
    std::cout << f() << std::endl;

    return 0;
}

The output of the line

std::cout << (*p)() << std::endl;

is 2, which is as I expected, of course.

But the output of the line

std::cout << f() << std::endl;

is 1. This was surprising me. I even was surprised that the assignment f = *p is allowed and doesn't cause an error.

I don't ask for a workaround, because I fixed it by a lambda.
My question is, what is happening when I do f = *p and why is the output 1 rather than 2?

I've reproduced the issue with gcc (MinGW) and Visual Studio 2019.
Further I want to mention that the output of

Bar b;
std::function<int(void)> f1 = b;
std::cout << f1() << std::endl;

is 2, again.

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • I wonder what you expected to happen in the guts of std::function for it to result in a call to Bar::opeeator(). How should the assignment have worked? – n. m. could be an AI Aug 30 '19 at 10:52
  • @n.m. I didn't expect anything, it was a bug, which I had to find in some code which was not written by me. – Rabbid76 Aug 30 '19 at 12:20

4 Answers4

26

Object slicing happens here.

The point is given f = *p;, p is of type std::shared_ptr<Foo>, then the type of *p is Foo& (instead of Bar&). Even the assignment operator of std::function takes argument by reference, but

4) Sets the target of *this to the callable f, as if by executing function(std::forward<F>(f)).swap(*this);.

Note that the F above is deduced as Foo& too. And the constructor of std::function takes argument by value, object slicing happens, the effect becomes that f is assigned from an object of type Foo which is slice-copied from *p.

template< class F > 
function( F f );
songyuanyao
  • 169,198
  • 16
  • 310
  • 405
12

This is regular slicing, hidden under a layer of std::function and std::shared_ptr.

f = *p;

is valid because *p is a callable object with an appropriate operator(), and that is one of the things you can wrap in a std::function.

The reason that it doesn't work is that it copies *p – and that is a Foo&, not a Bar&.

This adaptation of your last example would behave the same:

Bar b;
Foo& c = b;
std::function<int(void)> f1 = c;
std::cout << f1() << std::endl;
molbdnilo
  • 64,751
  • 3
  • 43
  • 82
12

Slicing

This is a case of slicing. The reason is assignment operator of std::function (as demonstrated in another answer as well) which states:

Sets the target of *this to the callable f, as if by executing function(std::forward(f)).swap(*this);. This operator does not participate in overload resolution unless f is Callable for argument types Args... and return type R. (since C++14)

https://en.cppreference.com/w/cpp/utility/functional/function/operator%3D

If you simplify and strip down the example - you can easily see what's going on:

Foo* p =  new Bar;

Foo f;
f = *p;//<-- slicing here since you deref and then copy the object

It looks like you were aiming at obtaining a pointer to the overridden virtual function - unfortunately, theres no easy way to unroll the virtual function lookup as that is implemented via a runtime lookup table. However an easy workaround might be to use a lambda to wrap (As the OP also mentions):

f = [p]{return (*p)();};

A more suitable solution could also be to just a use reference_wrapper:

f = std::ref(p);
darune
  • 10,480
  • 2
  • 24
  • 62
  • 3
    +1 for showing a workaround. However, the reason why this code "simplifies" to `Foo f = *p;` could use some more explanation. – Max Langhof Aug 30 '19 at 10:57
  • Sorry, but I didn't ask for a workaround. As I mentioned, I fixed the bug and actually I did it by a lambda. What I asked for is a detailed explanation. – Rabbid76 Aug 30 '19 at 12:23
  • 1
    @Rabbid76 Sure thing, I mentioned those workaround/solutions primarily for other users coming here. But I will update my answer. – darune Aug 30 '19 at 13:24
  • 1
    Or [`std::ref`](https://en.cppreference.com/w/cpp/utility/functional/ref) for short. – aschepler Aug 31 '19 at 13:57
0

The static type of the pointer p is Foo.

So in this statement

f = *p;

there left operand *p has the type Foo that is there is slicing.

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335