A Question about Copy Constructors in C++/CLI

Daniel O'Connell writes,

           

re: Followup: The Absence of MI

Hmm, Copy constructors...One would hope that the implementation and documentation is done *very* carefully(IE, marked as non-CLS compliant or implemented to work with ICloneable). I don't personally feel the pattern works well with non-C++ code(how many VB programmers are going to be instantly aware of it?), nor do I believe it works at all with interface based programming. Is there any other information on the implemntation or the advisories that will go along with copy constructors?

            re: Followup: The Absence of MI

My last comment wasn't well written, I mean how will copy constructors deal with classes written in C# inherited in C++ then inherited in VB? how will it work as opposed to basic aggregation(Stream(Stream stream), for example)?

His question deals with what I refer to as one of the intrinsics. The intrinsics are fundamental incompatibilities between the C++ and CLR object models. These are not random, but reflect a philosophical difference between the two models. Multiple Inheritance versus Single Inheritance + Interfaces is one such intrinsic. As that thread illustrated, the absence of direct support within the CLR object model for what within C++ is considered a natural mode of expression results in bereavement – that is, a sense of loss and inability to work efficiently.

In the C++ object model, an object is always a chunk of memory that is the size of its state members + alignment constraints of the host processor + the size of any internally generated implementation pods [such as a virtual table pointer in all C++ compilers that I am aware of, or the virtual base class table pointer that is specific to Visual C++]. And this object is not polymorphic: if you assign a derived class to a base class object, the derived portion is sliced off and discarded. The base class sub-objects are either bitwise or member-wise copied depending on the member types. To exhibit polymorphic behavior, one uses either pointers or references of the object’s type.

A reference, recall, is an ephemeral object. It binds to an object during initialization, then phase-shifts into an alias for that object. Assigning one reference to another is a deep copy – that is, either bitwise or member-wise copied. [Remember that initialization and assignment are distinct activities although they share a common operator, which can lead to confusion among programmers.] So it exhibits shallow copy during initialization, but deep-copy during assignment.

A pointer is a machine abstraction inherited from the C-language. On modern architectures, all pointers are (I believe) simply an address. A pointer to void, a pointer to int, a pointer to Widget, and a pointer to Fred all look the same in terms of the size and value domain. The type associated with the pointer object is an instruction to the compiler regarding the size and interpretation of the bits beginning at that address. A cast, in general, simply changes the interpretation of the size and set of bits. [I say generally because sometimes it changes the address as well.] We toggle between acting on the pointer and acting on the object addressed by that pointer syntactically by using the dereference operator – and this is a common source of confusion to those unused to the idiom. A pointer exhibits shallow-copy – that is, only the address is copied either in the initialization or assignment of one pointer with another. Under assignment, we can toggle the pointers such that a deep copy of the objects addressed are copied, and the value of the pointer itself is unchanged.

In the CLR, a managed reference type is a handle; that is, it is intrinsically polymorphic. The actual object is always on the managed heap as an unnamed entity allocated dynamically. Its removal from the heap is managed automatically through garbage collection and other than in exceptional instances is of no concern to the programmer. [This is the infamous non-deterministic finalization.] This is a staggeringly simplification, if you are a C++ programmer and are use to sweating the details of managing heap memory. [I suspect that programmers learning on a garbage collected system could not comfortably adapt to C++. It is like going from an automatic to a shift in driving a car. The migration path is really one way: going from the more complex to the simpler.] It exhibits shallow copy, and effecting a deep copy requires by convention the implementation of the Clone() member of the ICloneable interface which must be explicitly invoked by the user.

So, the default copy behavior between the two object models one of the intrinsics. It is a fundamental difference in the physics between the native Kansas and the managed Oz. And its absence in the original language design was felt by many as a bereavement.

The copy constructor mechanism in C++ is a way for the programming to gain control of the copying of one object with another when the default behavior is not appropriate. Let’s consider three questions:

  1. What is the default behavior and why was that chosen?
  2. Why is it necessary to sometimes manually override the default behavior?
  3. Why was the copy constructor and copy assignment operators chosen as the mechanism?

In C, or in later versions of C, the meaning of copying one aggregate object to another was defined as bitwise copy. In the original implementation of C++, this was the default behavior as well – one has to be able to have a C program behave and execute with equivalence. One reason it is necessary sometimes to override the default behavior is because pointers exhibit shallow copy, which can cause a number of problems that need to be explicitly programmed against [for example, unconditionally applying the delete operator to the pointer member within the destructor]. The copy constructor and copy assignment operator were the chosen mechanisms for in a sense trapping and handling the inappropriate default behavior because their invocation could be injected transparently by the compiler. Once this mechanism was introduced, bitwise copy was no longer appropriate when member class objects were complex types with their own copy operators that needed to be invoked. Thus, in the mid-1980s, the default mechanism shifted from bitwise- to memberwise copy. However, as a quality of implementation issue, an aggregate type exhibiting bitwise copy semantics is still bitwise copied.

In many circumstances, copy constructors are a royal pain. The classic example is when we return an aggregate object from within a function, as in

            Matrix operator+( const Matrix&, const Matrix& )

            {

                        Matrix result;

                        // do the math

                        return result;

            }

The last thing one actually wants here is a copy construction of the local result into the return value, but there is no way to express that directly in the language. There is a language extension in the g++ variant of C++ but that was actively opposed for adoption into the language. A huge bruhahaha erupted within the ISO/ANSI C++ committee at one point regarding the legality of optimizing away the copy constructor when safe to do so, as in

            Matrix mat = a + b;

but not in an assignment such as

            Matrix mat;

            mat = a + b;

which may or may not prove considerably expensive, but is generally always less efficient [I have to qualify it in order not to be called out for some corner case that may exist]. The problem with this optimization is that it becomes impossible to examine a piece of code and know its behavior – did the compiler optimize away the copy constructor? What happens if the destructor presumes that the copy constructor was invoked for some sort of increment/decrement handshake? Etc. etc. It has proved so confusing that Scott Meyers in More Effective C++ incorrectly described it [and as far as I know has never corrected it].

The CLR reflects a different programming philosophy and tradition – think of SmallTalk as a point of origin, although that is not deeply thought out. However, in the the presence of garbage collection and intrinsic polymorphism of managed reference types [with default shallow copy], the entire copy constructor/copy operator mechanism is not really necessary – at least in the opinion of the inventors of the CLR, and I tend to agree. However, it is felt as a bereavement by native C++ programmers, and it is a pattern of usage should one wish to chase the grail of transparent code that compiles to either native or managed. It falls between the intrinsics of multiple inheritance and that of deterministic finalization in my opinion as to its importance in being simulated.

Providing copy constructor support was not something I would have added to the C++/CLI language, and was not part of my prototype design [however, the urgency of my design was a proof of concept that the original language design was broken and that something better must replace it] and I had no part in its design or the thinking that led to its inclusion. I referred to it only because it is one of the intrinsics, and I was attempting to put multiple inheritance in a priority-based context.