开发者

Polymorphic objects on the stack?

开发者 https://www.devze.com 2023-03-03 04:35 出处:网络
In Why is there no base class in C++?, I quoted Stroustrup on why a common Object class for all classes is problematic in c++. In that quote there is the statement:

In Why is there no base class in C++?, I quoted Stroustrup on why a common Object class for all classes is problematic in c++. In that quote there is the statement:

Using a universal base class implies cost: Objects must be heap-allocated to be polymorphic;

I really didn't look twice at it, and since its on Bjarnes home page I would suppose a lot of eyes have scanned that sentence and reported any misstatements.

A commenter however pointed out that this is probably not the case, and in retrospect I can't find any good reason why this should be true. A short test case yields the expected result of VDerived::f().开发者_运维技巧

struct VBase {
    virtual void f() { std::cout <<"VBase::f()\n"; }
};

struct VDerived: VBase {
    void f() { std::cout << "VDerived::f()\n"; }
};

void test(VBase& obj) {
    obj.f();
}

int main() {
    VDerived obj;
    test(obj);
}

Of course if the formal argument to test was test(VBase obj) the case would be totally different, but that would not be a stack vs. heap argument but rather copy semantics.

Is Bjarne flat out wrong or am I missing something here?

Addendum: I should point out that Bjarne has added to the original FAQ that

Yes. I have simplified the arguments; this is an FAQ, not an academic paper.

I understand and sympathize with Bjarnes point. Also I suppose my eyes was one of the pairs scanning that sentence.


Looks like polymorphism to me.

Polymorphism in C++ works when you have indirection; that is, either a pointer-to-T or a reference-to-T. Where T is stored is completely irrelevant.

Bjarne also makes the mistake of saying "heap-allocated" which is technically inaccurate.

(Note: this doesn't mean that a universal base class is "good"!)


I think Bjarne means that obj, or more precisely the object it points to, can't easily be stack-based in this code:

int f(int arg) 
{ 
    std::unique_ptr<Base> obj;    
    switch (arg) 
    { 
    case 1:  obj = std::make_unique<Derived1      >(); break; 
    case 2:  obj = std::make_unique<Derived2      >(); break; 
    default: obj = std::make_unique<DerivedDefault>(); break; 
    } 
    return obj->GetValue(); 
}

You can't have an object on the stack which changes its class, or is initially unsure what exact class it belongs to.

(Of course, to be really pedantic, one could allocate the object on the stack by using placement-new on an alloca-allocated space. The fact that there are complicated workarounds is beside the point here, though.)

The following code also doesn't work as might be expected:

int f(int arg) 
{ 
    Base obj = DerivedFactory(arg); // copy (return by value)
    return obj.GetValue();
}

This code contains an object slicing error: The stack space for obj is only as large as an instance of class Base; when DerivedFactory returns an object of a derived class which has some additional members, they will not be copied into obj which renders obj invalid and unusable as a derived object (and quite possibly even unusable as a base object.)

Summing up, there is a class of polymorphic behaviour that cannot be achieved with stack objects in any straightforward way.


Of course any completely constructed derived object, wherever it is stored, can act as a base object, and therefore act polymorphically. This simply follows from the is-a relationship that objects of inherited classes have with their base class.


Having read it I think the point is (especially given the second sentence about copy-semantics) that universal base class is useless for objects handled by value, so it would naturally lead to more handling via reference and thus more memory allocation overhead (think template vector vs. vector of pointers).

So I think he meant that the objects would have to be allocated separately from any structure containing them and that it would have lead to many more allocations on heap. As written, the statement is indeed false.

PS (ad Captain Giraffe's comment): It would indeed be useless to have function

f(object o)

which means that generic function would have to be

f(object &o)

And that would mean the object would have to be polymorphic which in turn means it would have to be allocated separately, which would often mean on heap, though it can be on stack. On the other hand now you have:

template <typename T>
f(T o) // see, no reference

which ends up being more efficient for most cases. This is especially the case of collections, where if all you had was a vector of such base objects (as Java does), you'd have to allocate all the objects separately. Which would be big overhead especially given the poor allocator performance at time C++ was created (Java still has advantage in this because copying garbage collector are more efficient and C++ can't use one).


Bjarne's statement is not correct.

Objects, that is instances of a class, become potentially polymorphic by adding at least one virtual method to their class declaration. Virtual methods add one level of indirection, allowing a call to be redirected to the actual implementation which might not be known to the caller.

For this it does not matter whether the instance is heap- or stack-allocated, as long as it is accessed through a reference or pointer (T& instance or T* instance).

One possible reason why this general assertion slipped onto Bjarne's web page might be that it is nonetheless extremely common to heap-allocate instances with polymorphic behavior. This is mainly because the actual implementation is indeed not known to the caller who obtained it through a factory function of some sort.


I think he was going along the lines of not being able to store it in a base-typed variable. You're right in saying that you can store it on the stack if it's of the derived type because there's nothing special about that; conceptually, it's just storing the data of the class and it's derivatives + a vtable.

edit: Okay, now I'm confused, re-looking at the example. It looks like you may be right now...


I think the point is that this is not "really" polymorphic (whatever that means :-).

You could write your test function like this

template<class T>
void test(T& obj)
{
    obj.f();
}

and it would still work, whether the classes have virtual functions or not.


Polymorphism without heap allocation is not only possible but also relevant and useful in some real life cases.

This is quite an old question with already many good answers. Most answers indicate, correctly of course, that Polymorphism can be achieved without heap allocation. Some answers try to explain that in most relevant usages Polymorphism needs heap allocation. However, an example of a viable usage of Polymorphism without heap allocation seems to be required (i.e. not just purely syntax examples showing it to be merely possible).


Here is a simple Strategy-Pattern example using Polymorphism without heap allocation:

Strategies Hierarchy

class StrategyBase {
public:
    virtual ~StrategyBase() {}
    virtual void doSomething() const = 0;
};

class Strategy1 : public StrategyBase {
public:
    void doSomething() const override { std::cout << "Strategy1" << std::endl; }
};

class Strategy2 : public StrategyBase {
public:
    void doSomething() const override { std::cout << "Strategy2" << std::endl; }
};

A non-polymorphic type, holding inner polymorphic strategy

class A {
    const StrategyBase* strategy;
public:
    // just for the example, could be implemented in other ways
    const static Strategy1 Strategy_1;
    const static Strategy2 Strategy_2;

    A(const StrategyBase& s): strategy(&s) {}
    void doSomething() const { strategy->doSomething(); }
};

const Strategy1 A::Strategy_1 {};
const Strategy2 A::Strategy_2 {};

Usage Example

int main() {    
  // vector of non-polymorphic types, holding inner polymorphic strategy
  std::vector<A> vec { A::Strategy_1, A::Strategy_2 };

  // may also add strategy created on stack 
  // using unnamed struct just for the example
  struct : StrategyBase {
    void doSomething() const override {
      std::cout << "Strategy3" << std::endl;
    }
  } strategy3;

  vec.push_back(strategy3);

  for(auto a: vec) {
    a.doSomething();
  }
}

Output:

Strategy1
Strategy2
Strategy3

Code: http://coliru.stacked-crooked.com/a/21527e4a27d316b0


Let's assume we have 2 classes

class Base
{
public:
    int x = 1;
};

class Derived
    : public Base
{
public:
    int y = 5;
};

int main()
{
    Base o = Derived{ 50, 50 };

    std::cout << Derived{ o }.y;

    return 0;
}

The output will be 5 and not 50. The y is cut off. If the member variables and the virtual functions are the same, there is the illusion that polymorphism works on the stack as a different VTable is used. The example below illustrates that the copy constructor is called. The variable x is copied in the derived class, but the y is set by the initialization list of a temporary object.

The stack pointer has increased by 4 as the class Base holds an integer. The y will just be cut off in the assignment.

When using Polymorphism on the heap you tell the new allocator which type you allocate and by that how much memory on heap you need. With the stack this does not work. And neither memory is shrinking or increasing on the heap. As at the time of initialization you know what you're initializing and exact this amount of memory is allocated.

0

精彩评论

暂无评论...
验证码 换一张
取 消