Last Update:
Summary of Non-Regular Data Members in C++
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:
- C++ Smart Pointers Reference Card - C++ Stories
- 5 ways how unique_ptr enhances resource safety in your code - C++ Stories
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.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here:
Similar Articles:
- C++20 Ranges Algorithms - sorting, sets, other and C++23 updates
- C++20 Ranges Algorithms - 11 Modifying Operations
- C++20 Ranges Algorithms - 7 Non-modifying Operations
- Three Benchmarks of C++20 Ranges vs Standard Algorithms
- 20 Smaller yet Handy C++20 Features