PL

Reference Types in C++

This article discusses the meaning of reference types through a C++ written-test question, and how to define types with polymorphic behavior by disabling copyi…

Published

While going through old posts, I found this article: A tangle over a C++ written-test question; computer science students really have it rough. At the time, I probably found the syntax of Placement new [1] rather novel, so it left a deep impression on me. Now I feel that the written-test question in that article is quite well designed, so I am writing an article to specifically explain this issue — reference types in C++.

C++ Types Are Value Types by Default

A C++ class is a value type by default. We often look at a value type from the perspective of memory layout. By default, a value type can be copied; that is, a value type has a copy constructor and a copy assignment operator. A value-type object reflects its contents. When a copy occurs, two mutually independent objects are always produced, and modifying one object does not change the contents of the other.

C++ Can Also Define Reference Types

With certain settings, we can construct reference types in C++. Such types have polymorphic behavior and therefore can support object-oriented programming. We always view reference types with the goal of implementing polymorphic behavior, such as what their base class is, whether they have virtual functions, and so on. We should disable the copy constructor and copy assignment operator of a reference type, and use a virtual destructor. A reference-type object reflects its identity — what object is this a reference to? Therefore, reference types are also often called polymorphic types.

Defining Reference Types with C++

Although reference types in C# are all used through pointers or references, reference types in C++ are not subject to this restriction. However, when passing an object of a reference type, you can only pass a reference or pointer, not a value; otherwise, it is inconsistent with the intent of a reference type.

Therefore, what we need to do is:

  1. Disable the copy constructor.

  2. Disable the copy assignment operator.

  3. Explicitly provide a constructor.

  4. Explicitly provide a virtual destructor.

The sample code is as follows:

class MyRefType
{
private:
    MyRefType(const MyRefType &);
    MyRefType & operator=(const MyRefType &);
public:
    MyRefType() { }
    virtual ~MyRefType() { }
};

In this way, we define a reference-type object, MyRefType. Note that MyRefType can be allocated on the stack or on the heap, which is different from languages such as C# and Java. However, if you need to pass an object of MyRefType, you must pass a reference or a pointer; otherwise, a compile error will occur: (the error message varies by compiler)

Cannot access private member declared in class MyRefType.

The test case is as follows:

MyRefType CreateMyRefTypeInstance();
int main(void)
{
    MyRefType instance1;
    MyRefType instance2 = instance1;
    MyRefType instance3 = CreateMyRefTypeInstance();
    return 0;
}

Lines 19 and 20 will report errors at compile time.

New Syntax in C++11

C++11 introduced new syntax for deleting default functions or explicitly providing default functions. [2] An example using the new syntax is as follows: [3]

class MyRefType
{
    MyRefType(const MyRefType &) = delete;
    MyRefType & operator=(const MyRefType &) = delete;
public:
    MyRefType() = default;
    virtual ~MyRefType() = default;
};

Personally, I think using this approach to implement reference objects improves code readability by a level.

However, C++11 introduced a new semantic called Move, which can be used to transfer ownership of objects. What impact this has is still unknown. The "control of defaults: move and copy" section in Bjarne Stroustrup’s C++11 FAQ mentions some advice in this area.

Prohibiting Stack Allocation of Objects

After circling around, we return to that written-test question: how can we prohibit stack allocation of objects? Generally speaking, there are two methods: restrict access to the constructor, or restrict access to the destructor. At the same time, we should treat this object as an object of a reference type; otherwise, we could reasonably create a copy on the stack. I vaguely remember that Bjarne Stroustrup once mentioned in The Design and Evolution of C++ setting the destructor to protected (not verified), like this:

class HeapOnlyRefType1
{
private:
    HeapOnlyRefType1(const HeapOnlyRefType1 &);
    HeapOnlyRefType1 & operator=(const HeapOnlyRefType1 &);
public:
    HeapOnlyRefType1() { }
    void destroy(void) { delete this; }
protected:
    virtual ~HeapOnlyRefType1() { }
};

In this way, because the destructor cannot be used, the object naturally cannot be created on the stack. It should be noted that global variables and temporary objects of this type also cannot be created, because they ultimately still need to be destroyed, but the compiler cannot use the destructor. To avoid memory leaks, we use the destroy method to destroy objects of this type.

Another approach, which has greater flexibility and is also the one I am more accustomed to using, is to restrict this object by disabling the constructor, like this:

class HeapOnlyRefType2
{
private:
    HeapOnlyRefType2(const HeapOnlyRefType2 &);
    HeapOnlyRefType2 & operator=(const HeapOnlyRefType2 &);
protected:
    HeapOnlyRefType2() { }
public:
    static HeapOnlyRefType2 * CreateInstance(void) { return new HeapOnlyRefType2(); }
    virtual ~HeapOnlyRefType2() { }
};

In this way, in the future we can also control the creation of objects of this type by modifying the instance method. For example, we can use the Singleton Pattern or other creation patterns, or use some object policies, such as caching, and so on.

References


Supplement on 2012/12/3: The latest Visual Studio 2012 Update 1 now supports the syntax mentioned in [3].


3. The latest version of Visual Studio, 2012, still does not support this syntax, but GCC has supported this syntax since 4.4. Therefore, please use GCC 4.4 or later to practice this syntax.