Table of Contents

If you have a class with a regular data member like an integer or string, you’ll get all special member functions out of the box. But how about different types? In this article, we’ll look at other categories of members like unique_ptr, raw pointers, references, or const members.

Introduction  

In my book on “C++ Initialization” I recently wrote a chapter about so-called non-regular data members.

Here’s a definition based on cppreference - regular concept

A type is regular, that is, it is copyable, default constructible, and equality comparable. It is satisfied by types that behave similarly to built-in types like int, and that are comparable with ==.

For example:

class Product {
public:    
    Product() = default;
    explicit Product(std::string name, unsigned id)
    : name_(std::move(name))
    , id_(id)
    { }

private:
    std::string name_; 
    unsigned id_ { }; 
};

You can create, copy, move or assign an instance of the above class. The compiler provides all special member functions out of the box.

const  

If you have a const data member, then things get a bit more complicated:

class ProductConst {
public:    
    ProductConst() = default;
    explicit ProductConst(const char* name, unsigned id)
    : name_(name)
    , id_(id)
    { }

private:
    std::string name_; 
    const unsigned id_ { }; 
};

The instances of the class are:

  • default constructible (as id_ has some default value),
  • copy or move constructible,
  • not assignable. You cannot assign a new value. The compiler does not provide the copy and move assignment operators.

Pointers  

Let’s start with raw pointers… but only in rare cases as they are problematic and not safe:

class ProductPointer {
public:    
    ProductPointer() = default;
    explicit ProductPointer(std::string name, unsigned* pId)
    : name_(std::move(name))
    , pId_(pId)
    { }

private:
    std::string name_; 
    unsigned* pId_ { };
};

A raw pointer is actually a regular object, so you can copy or change it. But the semantics of the class with that member type is a bit more complex:

  • An instance of ProductPointer is default constructible,
  • ProductPointer has move operations
  • But the copy is problematic as it will be only a shallow copy - you can create many instances pointing to the same resource (like an allocated memory block). Still, it will be an issue to delete it and notify other owners safely.

It’s best to rely on smart pointers, depending on the program’s requirements.

class ProductUniquePointer {
public:    
    ProductUniquePointer() = default;
    explicit ProductUniquePointer(std::string name, unsigned Id)
    : name_(std::move(name))
    , pId_(std::make_unique<unsigned>(Id)) // make a copy
    { }

private:
    std::string name_; 
    std::unique_ptr<unsigned> pId_;
};

An instance of ProductUniquePointer has:

  • default constructor
  • default move operations
  • deleted copy constructor and copy assignment

And the shared_ptr version:

class ProductSharedPointer {
public:    
    ProductSharedPointer() = default;
    explicit ProductSharedPointer(std::string name, unsigned Id)
    : name_(name)
    , pId_(std::make_shared<unsigned>(Id)) // make a copy
    { }

private:
    std::string name_; 
    std::shared_ptr<unsigned> pId_;
};

This time an instance of ProductSharedPointer has:

  • default constructor
  • default move operations
  • default copy operations, but they are shallow. Still, “shallow” might be fine for the shared pointer, as the resource will be safely shared across many owners.

In both cases, I create a new pointer and copy the id argument.

Read more about smart pointers in:

References  

It’s a complicated thing:

class ProductRef {
public:    
    explicit ProductRef(std::string name, unsigned& id)
    : name_(std::move(name))
    , idRef_(id)
    { }

private:
    std::string name_; 
    unsigned& idRef_; 
};

Instances of the class have:

  • no default constructor available, a reference cannot be null/empty
  • the compiler provides a default copy and move constructors
  • assignment operator is deleted, as you cannot rebind a reference

Alternatively, you can try using std::reference_wrapper, which behaves like a reference, but can be rebounded to a different object.

Final table  

Thanks to type traits from the Standard Library, we can have a quick test showing the properties of such classes. The core function is:

template <typename T>
void ShowProps() {
    using namespace std;
    cout << typeid(T).name() << " props: \n";
    cout << "default constructible " << is_default_constructible_v<T> << " | ";
    cout << "copy assignable " << is_copy_assignable_v<T> << " | ";
    cout << "move assignable " << is_move_assignable_v<T> << '\n';
    cout << "copy constructible " << is_copy_constructible_v<T> << " | ";
    cout << "move constructible " << is_move_constructible_v<T> << '\n';
}

Using the above function template, I generated the following table:

Non-static data member type Default ctor Copy ctor Copy assign Move ctor Move assign
copyable, assignable, “regular” yes default default default default
const data member no, unless default value is set default custom only default custom only
pointer type yes default(shallow!) default(shallow!) default default
std::unique_ptr yes custom only custom only default default
std::shared_ptr yes default, shallow, but might be safe default, also shallow default default
reference type no default(shallow) custom only default custom only
std::reference_wrapper no default (shallow!) default(shallow) default default

For example, when your class has a const data member, the default constructor is unavailable (unless you assign some default value), the compiler can provide the copy and the move constructors, but default assignment operators are unavailable. “Custom only” means that the compiler cannot generate a default implementation, and the user has to provide some custom implementation.

Run the example @Compiler Explorer

Summary  

In this text, we covered a few types of (non-static) data members that might cause issues in the implementation. Some block a default constructor, and some copy or assignment operations. I hope that this article gave you some handy overview and basic ideas on how to work with instances of such classes. If you want to know more, then check out my book that contains far more examples and discussions: C++ Initialization Story by Bartłomiej Filipek (new free update later this week!).

Back to you

  • Do you use references, raw pointers non-moveable objects as data members?
  • Do you have techniques to avoid them?
  • What other “special” categories of objects do you use in your classes?

Share your feedback in the comments below.