virtual functions

Started by
22 comments, last by Erzengeldeslichtes 18 years, 5 months ago
In the following code

class Test1
{
    public:
    virtual void x()
    {
        std::cout << "1";
    }
};

class Test2 : public Test1
{
    public:
    void x()
    {
        std::cout << "2";
    }
};

int main(int argc, char *argv[])
{    

    Test2 test;
    std::vector<Test1> omg;
    std::vector<Test1*> omgp;
    omg.push_back(test);
    omgp.push_back(&test);
    test.x(); //this prints 2
    omg[0].x(); //this prints 1 while I'd like to to print 2
    omgp[0]->x(); //this prints 2
}

I'd like the "omg[0].x();" thing to print 2 as well. Is there a way to do that? (without pointers). Thanks
Advertisement
The virtual dispatch mechanism happens only through pointers.

In your example omg is a vector of Test1 objects... these objects are actual, concrete Test1's (as opposed to omgp which holds pointers to Test1s, so the actual object pointed to by any given Test1 pointer may actually be a Test2).

Since you don't provide a copy ct in your classes, the compiler is generating one for you, and when you are pushing-back your Test2 into a vector of Test1's you are most likely slicing your Test2 (this is a bad thing).

You can invoke Test1::x() from Test2::x() if you like, but I don't think that will be accomplishing what you want. It sounds like you want polymorphic behavior through non-pointers... which you can't really get in C++.
No, there isn't (maybe boost::any but that's a different thing). omg is a vector of Test1 objects so when you insert the Test2 object into it a Test1 is copy-constructed inside the vector. That means that what you have in the vector simply is a Test1 not a Test2, so you'll always get 1.
Welcome to the hell of object slicing. This is why garbage collection is so nice in the languages what use it (you can just store pointers to anything, and often implicitly do without realizing it).

One option is to wrap a pointer in your own class, to provide RAII and make the pointer 'ownership' more explicit - that way you have object rather than pointer behaviour, but still access to the polymorphism (although you still have to write -> rather than .). However, doing it properly and allowing for copying will require the 'virtual clone idiom'; I don't know a particularly nice way to do it, but here's something off the top of my head:

template <typename base>class virtual_wrapper {  base* wrapped;    public:  // Not sure about these  base& operator*() { return wrapped; }  const base& operator*() const { return wrapped; }  // There should be an operator-> too but I don't understand how those work  // Warning! Ctor will take ownership of the raw pointer.  virtual_wrapper(base* thing) : wrapped(thing) {}  virtual_wrapper(const virtual_wrapper& other) : wrapped(other->clone()) {}  virtual_wrapper& operator=(const virtual_wrapper& rhs) {    wrapped = other->clone();  }}class Test1{    public:    // etc.    // need to write this for each class :(      virtual Test1* clone() { return new Test1(this); }};class Test2 : public Test1{    public:    // etc.    // need to write this for each class :(      virtual Test2* clone() { return new Test2(this); }};// Now we can work withtypedef virtual_wrapper<Test1> Test;Test test(new Test2());Test other(test); // check copy constructionstd::vector<Test> omg;omg.push_back(test);omg.push_back(other);omg[1]->x();// Everything cleaned up nicely at end of scope.
The previous poster mentioned it, and I'd like to emphasize it.
You must avoid assigning a derived class to a base class by value.
When this happens, it's called slicing, because any data members of the derived class with be sliced off, and only the base class will be copied. Virtuality functions only through pointers and references. Be extremely careful when working with base class values; don't copy them as the base class, don't pass them by value to functions, don't insert them by value into containers, etc. Your omg vector is dangerous, and inserting a Test2 into it is a mistake. All of the information about Test2 is destroyed when you do that.

Oh, and if you want to attempt Zahlman's solution, be very careful. STL containers take some parameters by value instead of by const reference, so if your pointer wrapper's destructor deletes the pointer, you will always destroy your object when you attempt to add it to a container.
SlimDX | Ventspace Blog | Twitter | Diverse teams make better games. I am currently hiring capable C++ engine developers in Baltimore, MD.
class Test2 : public Test1{[...]    void x()[...]};// should be :class Test2 : public Test1{[...]    virtual void x()[...]};

else calling method x on an instance of test1 will always call Test1::x, as the VMT of the instance will NEVER contain a pointer on Test2::x, even for an instance of Test2.
So your test VMT is not initialised correctly for what you want to do.

std::vector should be able to store Test1/Test2 instances
Quote:Original post by TfpSly
*** Source Snippet Removed ***
else calling method x on an instance of test1 will always call Test1::x, as the VMT of the instance will NEVER contain a pointer on Test2::x, even for an instance of Test2.
So your test VMT is not initialised correctly for what you want to do.

This is incorrect. Although it is generally considered bad form to omit the virtual specifier on such derived functions it such a specifier is not required. "Once virtual, always virtual", as the saying goes.

Enigma
TfpSly: that virtual there is implicit. It's the same thing.

TfpSly: "std::vector should be able to store Test1/Test2 instances" - it "can", but it will almost never do what you want it to/expect it to

jpetrie: "The virtual dispatch mechanism happens only through pointers." ...and references


You guys need to be given a nice boost::shared_ptr

{   std::vector< boost::shared_ptr<Test1> > vec;   vec.push_back( boost::shared_ptr<Test1>( new Test1 ) );   vec.push_back( boost::shared_ptr<Test1>( new Test2 ) );   vec[0]->x(); //works correctly   vec[1]->x(); //works correctly   //everything gets cleaned up, too, no need to iterate through and delete each pointer}



CLEANER:

{   typedef boost::shared_ptr<Test1> pTest;   std::vector<pTest> vec;   vec.push_back( pTest( new Test1 ) );   vec.push_back( pTest( new Test2 ) );   vec[0]->x(); //works correctly   vec[1]->x(); //works correctly   //everything gets cleaned up, too, no need to iterate through and delete each pointer}


When you learn about shared_ptr, learn about weak_ptr, too. And don't forget std::auto_ptr, but don't try to put auto_ptr's into a container. Scott Meyers explains this stuff pretty well in his books.
Quote:Original post by RDragon1
   typedef boost::shared_ptr<Test1> pTest;



Slightly OT, nitpicky and not even important,
Test_ptr
is IMO a better name than
pTest
, because the latter communicates a pointer variable (to me, because I've seen too much pseudo-hungarian notation), while the former looks more like a type.
I very, very rarely declare a variable as a raw pointer, so for most intents and purposes a shared_ptr is as close as I usually see to a pointer. I use pType and wpType for shared_ptr and weak_ptr respectively. It's just what I like ;)

This topic is closed to new replies.

Advertisement