2

According to cppreference, a trivially copyable class should:

(class) has no virtual member functions;
(class) has no virtual base classes;

I don't understand the reason behind these requirements.

I tried to figure it out myself by doing:

#include <iostream>

struct virt
{
    int q;
    void virtual virt_func()
    {
        q += 2;
        std::cout << "base implementation: object value " << q << std::endl;
    }
};

struct virt_1 : public virt
{
    float w;
    void virt_func() override
    {
        w += 2.3;
        std::cout << "child 1 implementation: object value " << w << std::endl;
    }
};

struct virt_2 : public virt_1
{
    double e;
    void virt_func() override
    {
        e += 9.3;
        std::cout << "child 2 implementation: object value " << e << std::endl;
    }
};

int main()
{
    virt_2 * t = new virt_2();
    t->virt_func();
    void * p = malloc(sizeof(virt_2));
    
    memmove(p, t, sizeof(virt_2));

    static_cast<virt_2 *>(p)->virt_func();

    std::cout <<"End of a file" << std::endl;
    return 0;
}

and it works as it should, by printing:

child 2 implementation: object value 9.3
child 2 implementation: object value 18.6
End of a file

So, why, is what is effectively is a, no vtable pointer requirement is there? I mean, it's a simple pointer that can (should) be copied without any problem at all, right?!

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • 1
    What should happen if the vtable pointer isn't the correct pointer for the type being copied? It is an incorrect assumption that whatever vtable pointer exists in a `BaseClass&` is the vtable pointer for `BaseClass`. – Drew Dormann Apr 08 '22 at 16:13
  • 4
    Don't expect anything from _undefined behavior_ like you're using. – πάντα ῥεῖ Apr 08 '22 at 16:14
  • 4
    You have an assumption that `virtual` functions are implemented using vtable pointer, which is not guaranteed by standard. – Yksisarvinen Apr 08 '22 at 16:16
  • [std::memmove](https://en.cppreference.com/w/cpp/string/byte/memmove): "If the objects are potentially-overlapping or not TriviallyCopyable, the behavior of memmove is not specified and may be *undefined*." – Kevin Apr 08 '22 at 16:19
  • 1
    you cannot proovde or disproove the presence of UB by looking out output of some code, because UB means that the output can be anything, including what you expect – 463035818_is_not_an_ai Apr 08 '22 at 16:19
  • 2
    Does this answer your question? [Why would the behavior of std::memcpy be undefined for objects that are not TriviallyCopyable?](https://stackoverflow.com/questions/29777492/why-would-the-behavior-of-stdmemcpy-be-undefined-for-objects-that-are-not-triv) – Kevin Apr 08 '22 at 16:19
  • The cool thing about UB is you absolutely can exploit it, but only when you fully understand how it will behave on the target, after you've tested the ever-loving smurf out of it to ensure there are no gotrchas you missed in your analysis, and it's the last viable resort. Also helps if there can't be any bad consequences like death and dismemberment if it turn out you're still wrong. – user4581301 Apr 08 '22 at 16:21
  • I undid my duplicate close vote (there may be a better duplicate question though). The question is asking why a trivially copyable object can't have virtual functions/bases, not why `std::memmove` requires objects to be trivially copyable. – Kevin Apr 08 '22 at 16:23
  • 1
    Trivially copyable, among other things, allows "object can be written to a file, read from that file by another program, and the original object will be reconstructed". While the pointer may be correctly received, what that pointer points at may be completely different in the second program. – Peter Apr 08 '22 at 18:33

2 Answers2

3

Your example doesn't illustrate the dangers.

Imagine this:

virt_1 *copy_v1(virt_1 *t)
{
    void * p = malloc(sizeof(virt_1));
    memmove(p, t, sizeof(virt_1));
    return (virt_1 *)p;
}

called like this:

int main()
{
    virt_2 * v2 = new virt_2();
    virt_1 * v1 = copy_v1(v2);
    v1->virt_func();
}

What you've done here is create an object with the virt_2 virtual table but the object itself is too short and doesn't actually contain the e member.

Invoking virt_func() on this borked object will result in the proverbial undefined behavior.

dirck
  • 838
  • 5
  • 10
  • 2
    and what does this illustrate? – 463035818_is_not_an_ai Apr 08 '22 at 16:24
  • So if I ensures (somehow) that there are wouldnt be any slicing of an object or moving over its hierarchy type it will be 'fine'? – DisplayName Apr 08 '22 at 16:29
  • 3
    This code is compilable and will fail in some way, possibly catastrophically - you can try it. The problem with copy_v1 is that you can pass an instance of any child class of virt_1 and the copy will fail if the sizeof(child) != sizeof(virt_1). Inside copy_v1 you don't know the actual type of the object passed, so you can't copy the bytes - you don't know how many to copy. You've copied the vtable and functions in the vtable may reference memory off the end of the (partially) copied instance. – dirck Apr 08 '22 at 16:36
0

Think about what copying an object really means and what requirements are there for it to be safe.

One should be able to define the following method:

virt* copy_virt(virt* original)
{
    return new virt(*original);
}

And this method should be invocable with any valid pointer to an object of type virt.

How exactly is the used copy constructor defined? If virt is trivially copyable it should copy the exact underlying representation of original into the new instance. Including the vtable pointer.

Let's test this:

int main()
{
    virt_2 *k = new virt_2();
    k->q = 5;
    virt *c = copy_virt(k);
    c->virt_func();
}

Output:

base implementation: object value 7

The vtable pointer was not copied, therefore virt is not trivially copyable. Copying the underlying bytes does not have the same effect as invoking the copy constructor, because the copy constructor ensures there are no dangling pointers, like any copying should. (In this case the dangling pointer would come from invoking the wrong virtual function and trying to access w or e.)

You might try to argue that I copied an object of one type to another, but copy_virt takes a pointer and returns a pointer to the exact same type. C++ says it is not trivially copyable because you cannot ignore the possibility of original being an instance of a subclass.

Gergely
  • 159
  • 2
  • 14
  • That sliced original object and than created from it new instance. I mean, its using default, 'built in' C++ copy constructor that handles (compiler actually) virtual pointer 'correction' internally AFAIK. In my original post I used C functions that operate with a 'raw' memory (as far as it possible with modern OS) like malloc and memmove precisely because of that. – DisplayName Oct 09 '22 at 19:45
  • 1
    My point is the object is not trivially copyable because you cannot safely copy it using memcpy. Imagine if I instead implemented copy_virt using memcpy. Again, I **should** be able to invoke copy_virt with any pointer to the base type. However, as demonstrated by @dirck's answer it would leave the vtable pointer unchanged which causes undefined behaviour. This is the reason behind "why is the no vtable pointer requirement there" which was the original question. C++ cannot assume you only intend to invoke copy_virt with pointer to an object of the same dynamic type. – Gergely Oct 10 '22 at 06:42
  • Ehhhh, but pointer shouldnt be changed. Yes if you slice original object than vtable pointer would point to list of functions of original object type, not the sliced one. This, and only this, could cause segfault. But I copied object fully, without slicing it. In original post I copied virt_2 by memmove and cast pointer type, to which object was copied, from void to virt_2 pointer. I havent caused any dynamic polimorphism related questions by doing that. – DisplayName Oct 10 '22 at 11:05
  • The fact it can work in certain cases does not mean it works in all cases and it is not trivially copyable because there **are** cases where it can cause undefined behaviour. Your question was, I believe, is why is there a "no vtable pointer requirement". Both answers given try to justify the "why". – Gergely Oct 10 '22 at 11:14
  • Maybe my original question was misleading. But than again, its not stated clearly on cppreference that thats the reason behind vtable pointer, as 'copy' there used in wide meaning of this word like 'copy in general'. On the other hand, how often it required to copy object 'partially'? I dont know cases where it required to copy object as an 'ancestor'. – DisplayName Oct 10 '22 at 16:41