C++ Copy Constructors: must I spell out all member variables in the initializer list?
Asked Answered
Y

2

9

I have some pretty complicated objects. They contain member variables of other objects. I understand the beauty of copy constructors cascading such that the default copy constructor can often work. But, the situation that may most often break the default copy constructor (the object contains some member variables which are pointers to its other member variables) still applies to a lot of what I've built. Here's an example of one of my objects, its constructor, and the copy constructor I've written:

struct PhaseSpace {
  PhaseSpace();

private:
  std::string file_name;              ///< Name of the file from which these coordinates (and
                                      ///<   perhaps velocities) derived.  Empty string indicates
                                      ///<   no file.
  int atom_count;                     ///< The number of atoms in the system
  UnitCellType unit_cell;             ///< The type of unit cell
  Hybrid<double> x_coordinates;       ///< Cartesian X coordinates of all particles
  Hybrid<double> y_coordinates;       ///< Cartesian Y coordinates of all particles
  Hybrid<double> z_coordinates;       ///< Cartesian Z coordinates of all particles
  Hybrid<double> box_space_transform; ///< Matrix to transform coordinates into box space (3 x 3)
  Hybrid<double> inverse_transform;   ///< Matrix to transform coordinates into real space (3 x 3)
  Hybrid<double> box_dimensions;      ///< Three lengths and three angles defining the box (lengths
                                      ///<   are given in Angstroms, angles in radians)
  Hybrid<double> x_velocities;        ///< Cartesian X velocities of all particles
  Hybrid<double> y_velocities;        ///< Cartesian Y velocities of all particles
  Hybrid<double> z_velocities;        ///< Cartesian Z velocities of all particles
  Hybrid<double> x_forces;            ///< Cartesian X forces acting on all particles
  Hybrid<double> y_forces;            ///< Cartesian Y forces acting on all particles
  Hybrid<double> z_forces;            ///< Cartesian Z forces acting on all particles
  Hybrid<double> x_prior_coordinates; ///< Previous step Cartesian X coordinates of all particles
  Hybrid<double> y_prior_coordinates; ///< Previous step Cartesian Y coordinates of all particles
  Hybrid<double> z_prior_coordinates; ///< Previous step Cartesian Z coordinates of all particles

  /// All of the above Hybrid objects are pointers into this single large array, segmented to hold
  /// each type of information with zero-padding to accommodate the HPC warp size.
  Hybrid<double> storage;

  /// \brief Allocate space for the object, based on a known number of atoms
  void allocate();
};

//-------------------------------------------------------------------------------------------------
PhaseSpace::PhaseSpace() :
    file_name{std::string("")},
    atom_count{0},
    unit_cell{UnitCellType::NONE},
    x_coordinates{HybridKind::POINTER, "x_coordinates"},
    y_coordinates{HybridKind::POINTER, "y_coordinates"},
    z_coordinates{HybridKind::POINTER, "z_coordinates"},
    box_space_transform{HybridKind::POINTER, "box_transform"},
    inverse_transform{HybridKind::POINTER, "inv_transform"},
    box_dimensions{HybridKind::POINTER, "box_dimensions"},
    x_velocities{HybridKind::POINTER, "x_velocities"},
    y_velocities{HybridKind::POINTER, "y_velocities"},
    z_velocities{HybridKind::POINTER, "z_velocities"},
    x_forces{HybridKind::POINTER, "x_forces"},
    y_forces{HybridKind::POINTER, "y_forces"},
    z_forces{HybridKind::POINTER, "z_forces"},
    x_prior_coordinates{HybridKind::POINTER, "x_prior_coords"},
    y_prior_coordinates{HybridKind::POINTER, "y_prior_coords"},
    z_prior_coordinates{HybridKind::POINTER, "z_prior_coords"},
    storage{HybridKind::ARRAY, "phase_space_data"}
{}

//-------------------------------------------------------------------------------------------------
PhaseSpace::PhaseSpace(const PhaseSpace &original) :
    file_name{original.file_name},
    atom_count{original.atom_count},
    unit_cell{original.unit_cell},
    x_coordinates{original.x_coordinates},
    y_coordinates{original.y_coordinates},
    z_coordinates{original.z_coordinates},
    box_space_transform{original.box_space_transform},
    inverse_transform{original.inverse_transform},
    box_dimensions{original.box_dimensions},
    x_velocities{original.x_velocities},
    y_velocities{original.y_velocities},
    z_velocities{original.z_velocities},
    x_forces{original.x_forces},
    y_forces{original.y_forces},
    z_forces{original.z_forces},
    x_prior_coordinates{original.x_prior_coordinates},
    y_prior_coordinates{original.y_prior_coordinates},
    z_prior_coordinates{original.z_prior_coordinates},
    storage{original.storage}
{
  // Set the POINTER-kind Hybrids in the new object appropriately.  Even the resize() operation
  // inherent to "allocate" will pass by with little more than a check that the length of the data
  // storage array is already what it should be.
  allocate();
}

EDIT: here's the allocate() member function if it helps explain anything... with the storage array already having been allocated to the same length as the original and given a deep copy by the Hybrid object type's own copy-assignment constructor, all that remains is to set this PhaseSpace object's own POINTER-kind Hybrid objects to the appropriate locations in the ARRAY-kind Hybrid object storage.

//-------------------------------------------------------------------------------------------------
void PhaseSpace::allocate() {
  const int padded_atom_count  = roundUp(atom_count, warp_size_int);
  const int padded_matrix_size = roundUp(9, warp_size_int);
  storage.resize((15 * padded_atom_count) + (3 * padded_matrix_size));
  x_coordinates.setPointer(&storage,                            0, atom_count);
  y_coordinates.setPointer(&storage,            padded_atom_count, atom_count);
  z_coordinates.setPointer(&storage,        2 * padded_atom_count, atom_count);
  box_space_transform.setPointer(&storage,  3 * padded_atom_count, 9);
  inverse_transform.setPointer(&storage,   (3 * padded_atom_count) +      padded_matrix_size, 9);
  box_dimensions.setPointer(&storage,      (3 * padded_atom_count) + (2 * padded_matrix_size), 6);
  const int thus_far = (3 * padded_atom_count) + (3 * padded_matrix_size);
  x_velocities.setPointer(&storage,        thus_far,                           atom_count);
  y_velocities.setPointer(&storage,        thus_far +      padded_atom_count,  atom_count);
  z_velocities.setPointer(&storage,        thus_far + (2 * padded_atom_count), atom_count);
  x_forces.setPointer(&storage,            thus_far + (3 * padded_atom_count), atom_count);
  y_forces.setPointer(&storage,            thus_far + (4 * padded_atom_count), atom_count);
  z_forces.setPointer(&storage,            thus_far + (5 * padded_atom_count), atom_count);
  x_prior_coordinates.setPointer(&storage, thus_far + (6 * padded_atom_count), atom_count);
  y_prior_coordinates.setPointer(&storage, thus_far + (7 * padded_atom_count), atom_count);
  z_prior_coordinates.setPointer(&storage, thus_far + (8 * padded_atom_count), atom_count);
}

The comments are from the actual code, although I have (perhaps obviously) omitted a great deal of the details in the original struct definition. My question is whether the copy constructor needs such a detailed initializer list, or if there is a more elegant (and less error-prone) shorthand for making a custom copy constructor like this. Once I have an answer to this issue, I've got some much bigger objects with the same situation.

Cheers!

Yazzie answered 30/1, 2022 at 1:53 Comment(9)
Possible dupe: How to use both default and custom copy constructor in C++?Mehitable
It looks like you could replace all of those member-variables with an enum and an array, e.g. enum {VAL_X_COORDINATES = 0, VAL_Y_COORDINATES, VAL_Z_COORDINATES, VAL_BOX_SPACE_TRANSFORM, [...], NUM_VALS}; std::array<NUM_VALS, Hybrid<double> > vals; ... then you'd have just one member-variable to deal with rather than 15, and your copy-constructor's initializer-list would be much shorter. You'd also gain the ability to iterate over all the values, which is often useful.Alguire
@Dúthomhas thanks for your comment. While I don't think this is quite a dupe (and perhaps after nine years it's time to refresh), I do see merit in the solutions proposed by the respondents at the link you provided. My reaction is that the 2-3 solutions proposed would indeed make the copy constructor more elegant, albeit at the expense of more gobbledegook whenever the object was used by a developer. Perhaps I am fighting against a no-free-lunch situation and there will be pain one place or elsewhere. If that is the case, I may choose to take the tedium upon myself.Yazzie
One common practice of dealing with 3d objects might be to group xyz values of the same properties into another struct, such as Vec3(a mathematical 3d vector). It can further help you when you need to define 3d/matrix related math functions, like cross product.Irremeable
One thing that I may not have said much about thus far is that this is a CUDA/C++ code and the Hybrid object contains pointers to memory in both the C++ /CPU and CUDA / device layers, with means to transfer the data between them and query either array in various ways. I appreciate the note about pointers and efficiency--indeed, I want to write structs to be as clean and performant as possible. In this situation, it's hard to imagine how I would arrange the object(s) to be more performant, however. Each Hybrid has a pointer in it. Choosing one or the other is the same reference speed.Yazzie
The CUDA nature of the code dictates whether I use vec3 (and if I want to fuse the attributes, which I do in some cases, I tend to opt for vec4 (float4, double4) tuple types as these have performant read / write methods in CUDA intrinsics, whereas vec3 is a chimeric two-tuple and a one-tuple under the hood. All of these structs have methods for collecting pointers to their various data arrays at the C++ or CUDA level, so when I take things to the CUDA layer I take the pointer collection (the "abstract") and pass it (by value) to my kernel as a launch argument.Yazzie
Your default constructor doesn't call allocate. Don't the pointers to storage need to be set in the default constructor as well?Prem
@Prem good question, and I had neglected to do it due to the ambiguity of the case when there are zero atoms. The Hybrid object is set to not let me set it as a pointer to data that would fall off the end of an array, although it's not clear whether a pointer managing zero elements set to the Nth element of an array of size N is out of bounds. The role of the default constructor is to prepare the PhaseSpace object to receive coordinates from a function buildFromFile(const std::string &file_name), which is invoked by a different constructor or can be called after a file existence check.Yazzie
@Prem thanks for spotting that, I have now fixed that issue in a handful of objects. Always good to make a pass and clean up things like this.Yazzie
A
7

C++ Copy Constructors: must I spell out all member variables in the initializer list?

Yes, if you write a user defined copy constructor, then you must write an initialiser for every sub object - unless you wish to default initialise them, in which case you don't need any initialiser - or if you can use a default member initialiser.

the object contains some member variables which are pointers to its other member variables)

This is a design that should be avoided when possible. Not only does this force you to define custom copy and move assignment operators and constructors, but it is often unnecessarily inefficient.

But, in case that is necessary for some reason - or custom special member functions are needed for any other reason - you can achieve clean code by combining the normally copying parts into a separate dummy class. That way the the user defined constructor has only one sub object to initialise.

Like this:

struct just_data {
    // many members... details don't matter
    R just_data_1;
    S just_data_2;
    T just_data_3;
    U just_data_4;
    V just_data_5;
};

struct fancy_copying : just_data
{
    fancy_copying(const fancy_copying& other)
        : just_data(other.just_data)
    {
        do_the_fancy();
    }
};
Affinitive answered 30/1, 2022 at 2:35 Comment(0)
F
0

Well, no, it is not essential that you list all members in the initialiser list of a constructor. But, if you don't, you need to accept and somehow mitigate the consequences.

Initialisation of some members is mandatory. For example, it is necessary to initialise a reference when creating it (to make it refer correctly to something). I assume you are not trying to leave such members uninitialised.

Any member that you don't explicitly initialise in the constructor initialiser list will be default-initialised.

For basic types (e.g int, double, raw pointers) default-initialisation means zero initialisation if the object is a global but, otherwise, means "leave uninitialised" so accessing its value gives undefined behaviour. So, if such a member is uninitialised, the only thing you can do with it is assign its value. Doing anything that accesses its original (uninitialised) value (incrementing, adding a value to it, multiplying, copying, etc) gives undefined behaviour.

Even a basic search will turn up all sorts of explanations why it is (usually) advisable to avoid introducing undefined behaviour.

For struct/class types, default initialisation means invoking a constructor that can accept no arguments. If that member has no such constructor it will be necessary to explicitly initialise that member (to either list-initialise it or call a constructor it actually has). The reasoning is then recursive (depending on what members each member has).

Practically, if it makes sense to leave a class member uninitialised (as distinct from default-constructed) in the constructor, you probably need to ask why that class has that member at all. If you want to delay initialisation of members to after construction of the object, you need to question why you can't delay creating that instance of the class.

Faber answered 30/1, 2022 at 4:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.