Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D struct copy constructor

Is it possible to call struct copy constructor explicitly, like in C++? Can I write something like this:

struct foo {
    void bar() {}
}

foo f;
foo(f).bar();

Or I always need to assign new value to some varialbe?

like image 910
mcferden Avatar asked Dec 24 '22 03:12

mcferden


2 Answers

Well, technically, D doesn't even have copy constructors. Rather, structs can have postblit constructors. e.g.

struct S
{
    this(this)
    {
    }
}

In general, D tries to move structs as much as possible and not copy them. And when it does copy them, it does a bitwise copy of the struct and then runs the postblit constructor (if there is one) to mutate the struct after the fact to do stuff that needs to be done beyond a bitwise copy - e.g. if you want a deep copy of a member

struct S
{
    this(this)
    {
        if(i !is null)
            i = new int(*i);
    }

    int* i;
}

A copy constructor (in C++), on the other hand, constructs a new struct/class and initializes each member with a copy of the corresponding member in the struct/class being copied - or with whatever it's initialized with in the copy constructor's initializer list. It doesn't copy and then mutate like happens with D's postblit constructor. So, a copy constructor and a postblit constructor are subtly different.

One of the side effects of this is that while all structs/class in C++ have copy constructors (the compiler always generates one for you if you don't declare one), not all structs in D have postblit constructors. In fact, most don't. The compiler will generate one if the struct contains another struct that has a postblit constructor, but otherwise, it's not going to generate one, and copying is just going to do a bitwise copy. And, if there is no postblit construct, you can't call it implicitly or explicitly.

Now, if we compile this

struct A
{
}
pragma(msg, "A: " ~ __traits(allMembers, A).stringof);

it prints

A: tuple()

A has no members - be it member variables or functions. None have been declared, and the compiler has generated none.

struct B
{
    A a;
    string s;
}
pragma(msg, "B: " ~ __traits(allMembers, B).stringof);

prints

B: tuple("a", "s")

It has two members - the explicitly declared member variables. It doesn't have any functions either. Declaring member variables is not a reason for the compiler to generate any functions. However, when we compile

struct C
{
    this(this)
    {
        import std.stdio;
        writeln("C's postblit");
    }

    int i;
    string s;
}
pragma(msg, "C: " ~ __traits(allMembers, C).stringof);

it prints

C: tuple("__postblit", "i", "s", "__xpostblit", "opAssign")

Not only are its two member variables listed, but it also has __postblit (which is the explicitly declared postblit constructor) as well as __xpostblit and opAssign. __xpostblit is the postblit constructor generated by the compiler (more on that in a second), and opAssign is the assignment operator which the compiler generated (which is needed, because C has a postblit constructor).

struct D
{
    C[5] sa;
}
pragma(msg, "D: " ~ __traits(allMembers, D).stringof);

prints

D: tuple("sa", "__xpostblit", "opAssign")

Note that it has __xpostblit but not __postblit. That's because it does not have an explitly declared postblit constructor. __xpostblit was generated to call the postblit constructor of each of the member variables. sa is a static array of C's, and C has a postblit constructor. So, in order to copy sa properly, the postblit constructor for C must be called on each of the elements in sa. D's __xpostblit does that. C also has __xpostblit, but it doesn't have any members with postblit constructors, so its __xposblit just calls its __postblit.

struct E
{
    this(this)
    {
        import std.stdio;
        writeln("E's postblit");
    }

    C c;
}
pragma(msg, "E: " ~ __traits(allMembers, E).stringof);

prints

E: tuple("__postblit", "c", "__xpostblit", "opAssign")

So, E - like C - has both __postblit and __xpostblit. __postblit is the explicit postblit constructor, and __xpostblit is the one generated by the compiler, However, in this case, the struct actually has member variables with a postblit constructor, so __xpostblit has more to do than just call __postblit.

If you had

void main()
{
    import std.stdio;
    C c;
    writeln("__posblit:");
    c.__postblit();
    writeln("__xposblit:");
    c.__xpostblit();
}

it would print

__posblit:
C's postblit
__xposblit:
C's postblit

So, there's no real difference between the two, whereas if you had

void main()
{
    import std.stdio;
    D d;
    writeln("__xposblit:");
    d.__xpostblit();
}

it would print

__xposblit:
C's postblit
C's postblit
C's postblit
C's postblit
C's postblit

Notice that C' postblit gets called 5 times - once for each element in D's member, sa. And we couldn't call __postblit on D, because it doesn't have an explicit postblit constructor - just the implicit one.

void main()
{
    import std.stdio;
    E e;
    writeln("__posblit:");
    e.__postblit();
    writeln("__xposblit:");
    e.__xpostblit();
}

would print

__posblit:
E's postblit
__xposblit:
C's postblit
E's postblit

And in this case, we can see that __postblit and __xpostblit are different. Calling __postblit just calls the explicitly declared postblit constructor, whereas __xpostblit calls it and the postblit constructors of the member variables.

And of course, since, A and B don't have posblit constructors and no members that have them, it would be illegal to call either __postblit or __xpostblit on them.

So, yes, you can call the postblit constructor explicitly - but only if it has one, and you almost certainly shouldn't be calling it. If a function starts with __, or it's one of the overloaded operators (and thus starts with op), then it almost never should be called explicitly - and that includes the postblit constructor. But if you do find a legitimate reason to call it, remember that you're probably going to want to call __xpostblit rather than __postblit, otherwise the postblit for the member variables won't be run. You can test for it by doing __traits(hasMember, S1, "__xpostblit") or by using the badly named hasElaborateCopyConstructor from std.traits (most code should use hasElaborateCopyConstructor, since it's more idiomatic). If you want to call __postblit for some reason, you'll need to test for it with __traits rather than std.traits though, because pretty much nothing outside of druntime cares whether a type declared __postblit. The stuff that does care about posblit constructors cares about __xpostblit, since that can exist whether __postblit was declared or not.

like image 103
Jonathan M Davis Avatar answered Jan 03 '23 10:01

Jonathan M Davis


D doesn't have copy constructors per se, but you could call the implicit constructor with the contents of the existing one (which would create a shallow copy at least) with

foo(f.tupleof).bar()

The f.tupleof gives the list of struct members in a form that is suitable for automatic expansion to a function argument list.

like image 44
Adam D. Ruppe Avatar answered Jan 03 '23 11:01

Adam D. Ruppe