Goal: Write complete, expressive, and efficient types
A type is a pattern for storing and modifying objects.
struct
and class
are mechanisms for implementing types but can also be used for other purposesAn object is a representation of an entity as a value in memory.
In addition, for a given type there are other essential operations
The computational basis for a type is a finite set of procedures that enable the construction of any other procedure on the type
There is a set of procedures whose inclusion in the computational basis of a type lets us place objects in data structures and use algorithms to copy objects from one data structure to another. We call types having such a basis regular since their use guarantees regularity of behavior and, therefore, interoperability.
— (Stepanov & McJones)
Two objects are equal iff they represent the same entity
For all $op$, which modifies its operand, and given $b = c$:
\begin{align} b & \to a, op(a) \implies a \neq b \wedge b = c. && \text{(copies are disjoint)} \end{align}namespace v0 {
instrumented f() {
instrumented r;
return r;
}
} // namespace v0
Question: How many copies? How many moves?
instrumented a = f();
{
using namespace v0;
instrumented a = f();
}
= default
to ensure it is presentnamespace v0 {
class my_type {
// members
public:
my_type(const my_type&) = default;
};
} // namespace v0
namespace v1 {
class my_type {
// members
public:
my_type(const my_type&) = default;
my_type& operator=(const my_type&) = default;
};
} // namespace v1
If the representation of an object is unique, then equality can be implemented as part-wise equality
C++20 provides member-wise equality (and inequality) by explicitly defaulting operator==()
For C++11 until C++20
std::tie()
as a simple mechanism to implement equalityoperator==()
as a non-member operatorfriend
declaration may be used to implement directly in the class definitioninline
is impliednamespace v2 {
// C++20
class my_type {
int _a = 0;
int _b = 42;
public:
my_type(const my_type&) = default;
my_type& operator=(const my_type&) = default;
bool operator==(const my_type&) const = default;
};
} // namespace v2
namespace v2 {
// C++17
class my_type {
int _a = 0;
int _b = 42;
auto underlying() const { return std::tie(_a, _b); }
public:
my_type(const my_type&) = default;
my_type& operator=(const my_type&) = default;
friend bool operator==(const my_type& a, const my_type& b) {
return a.underlying() == b.underlying();
}
friend bool operator!=(const my_type& a, const my_type& b) { return !(a == b); }
};
} // namespace v2
operator==()
as representational equality2 worst case, if equal.
namespace v3 {
class my_type {
int _val; // local part
//...
};
} // namespace v3
Exercise: Implement a type with a remote part holding a pair of integers.
// 02-types.hpp
namespace v4 {
class my_type {
struct implementation; // forward declaration
/* <some_type> _remote; */ // remote part
public:
my_type(int x, int y);
~my_type();
my_type(const my_type&);
my_type& operator=(const my_type&);
};
} // namespace v4
// 02-types.cpp
// #include "02-types.hpp" // first include
// other includes
namespace v4 {
struct my_type::implementation {
int _x;
int _y;
};
/*
Fill in the rest...
*/
} // namespace v4
// 02-types.hpp
#include <memory>
namespace v41 {
class my_type {
struct implementation; // forward declaration
struct deleter {
void operator()(implementation*) const;
};
std::unique_ptr<implementation, deleter> _remote; // remote part
public:
my_type(int x, int y);
~my_type() = default;
my_type(const my_type&);
my_type& operator=(const my_type&);
};
} // namespace v4
// 02-types.cpp
// #include "02-types.hpp" // first include
// other includes
namespace v41 {
struct my_type::implementation {
int _x;
int _y;
};
my_type::my_type(int x, int y) : _remote{new implementation{x, y}} {}
my_type::my_type(const my_type& a) : _remote{new implementation{*a._remote}} {}
my_type& my_type::operator=(const my_type& a) {
*_remote = *a._remote;
return *this;
}
void my_type::deleter::operator()(implementation* p) const { delete p; }
} // namespace v41
{
using namespace v41;
my_type a{10, 20};
my_type b = a;
a = b;
}
Exercise: Implement operator==()
on my_type.
// 02-types.hpp
#include <memory>
namespace v42 {
class my_type {
struct implementation; // forward declaration
struct deleter {
void operator()(implementation*) const;
};
std::unique_ptr<implementation, deleter> _remote; // remote part
public:
my_type(int x, int y);
~my_type() = default;
my_type(const my_type&);
my_type& operator=(const my_type&);
friend bool operator==(const my_type&, const my_type&);
friend bool operator!=(const my_type& a, const my_type& b) { return !(a == b); }
};
} // namespace v42
#include <tuple>
namespace v42 {
struct my_type::implementation {
int _x;
int _y;
auto underlying() const { return std::tie(_x, _y); }
};
my_type::my_type(int x, int y) : _remote{new implementation{x, y}} {}
my_type::my_type(const my_type& a) : _remote{new implementation{*a._remote}} {}
my_type& my_type::operator=(const my_type& a) {
*_remote = *a._remote;
return *this;
}
void my_type::deleter::operator()(implementation* p) const { delete p; }
bool operator==(const my_type& a, const my_type& b) {
return a._remote->underlying() == b._remote->underlying();
}
} // namespace v42
{
using namespace v42;
my_type a{10, 20};
my_type b = a;
assert(a == b);
b = my_type{5, 30};
assert(a != b);
a = b;
assert(a == b);
}
std::regular<T>
¶The C++20 standard defines the concept std::regular
There are different categories of safety:
memory safety
thread safety
A thread safe operation may be executed concurrently with other operations on the same object(s) without the possibility of a race condition (data or logical race) resulting an an object which is not full-formed.
exception safety
An exception safe operation is one which after an exception any objects being operated on are in a fully-formed state. C++ refers to this as the strong exception guarantee.
An operation satisfying the basic exception guarantee ensures that after an exception any objects being operated on are partially-formed.
An operation is efficient if there is no way to implement it to use fewer resources:
Unless otherwise specified, we will use efficiency to mean time efficiency
A basis is efficient if and only if any procedure can be implemented as efficiently using it as an equivalent procedure written in terms of an alternative basis.
Making all data members public ensures an efficient basis, but may be unsafe
In fact, we can prove that some operations cannot be implemented both efficiently and safely
The canonical example is in-situ sort, although it is true of any in-situ permutation
In C++, explicit move
is both unsafe and inefficient
Strive to make operations safe and efficient
Only sacrifice safety for efficiency with good (measurable) reason
Exercise: Implement move-construction and move-assignment operators on my_type
.
namespace v43 {
class my_type {
struct implementation; // forward declaration
struct deleter {
void operator()(implementation*) const;
};
std::unique_ptr<implementation, deleter> _remote; // remote part
public:
my_type(int x, int y);
~my_type() = default;
my_type(const my_type&);
my_type& operator=(const my_type&);
my_type(my_type&&) noexcept = default; // <--
my_type& operator=(my_type&&) = default; // <--
friend bool operator==(const my_type&, const my_type&);
friend bool operator!=(const my_type& a, const my_type& b) { return !(a == b); }
};
} // namespace v43
my_type& my_type::operator=(const my_type& a) {
*_remote = *a._remote;
return *this;
}
Question: What happens if we assign-to a moved from object?
namespace v43 {
struct my_type::implementation {
int _x;
int _y;
auto underlying() const { return std::tie(_x, _y); }
};
my_type::my_type(int x, int y) : _remote{new implementation{x, y}} {}
my_type::my_type(const my_type& a) : _remote{new implementation{*a._remote}} {}
my_type& my_type::operator=(const my_type& a) { // <--
if (!_remote) *this = my_type{a};
*_remote = *a._remote;
return *this;
}
void my_type::deleter::operator()(implementation* p) const { delete p; }
bool operator==(const my_type& a, const my_type& b) {
return a._remote->underlying() == b._remote->underlying();
}
} // namespace v43
{
using namespace v43;
my_type a{10, 20};
my_type b{move(a)};
a = my_type{5, 30};
assert((b == my_type{10, 20}));
assert((a == my_type{5, 30}));
}
double x = 0.0 / 0.0; // explicitly undefined
x
{
int x; // unspecified
display(x); // undefined behavior!
}
string x = "hello world";
string y = move(x); // unspecified
x
unique_ptr<int> x = make_unique<int>(42);
unique_ptr<int> y = move(x); // safe. x is guaranteed to be == nullptr
(x == nullptr)
op(rv); rv.~T();
is safestd::move()
is equivalent to static_cast<T&&>()
forward<>
or move()
them into place.namespace v5 {
class example {
string _str;
public:
template <class T>
example(T&& a) : _str{std::forward<T>(a)} {}
// or
example(string a) : _str{std::move(a)} {}
};
} // namespace v5
{
using namespace v5;
// Don't
string str{"Hello World!"};
example item1{move(str)};
// Do
example item2{"Hello World!"};
}
What should the state be of a default constructed object?
A common use case of a default constructed object is to create the object before we have a value to give to it:
{
using namespace v11;
string s;
if (predicate()) s = "Hello";
else s = "World";
}
{
using namespace v11;
string s1;
string s2;
tie(s1, s2) = get_pair();
}
{
using namespace v11;
string s = predicate() ? "Hello" : "World";
}
{
using namespace v11;
auto [s1, s2] = get_pair();
}
constexpr
sizeof()
the objectstd::optional<>
std::regular<>
for historical reasonsmove
as a basis operationmove
was done with default-construction and swapnamespace v44 {
class my_type {
struct implementation; // forward declaration
struct deleter {
void operator()(implementation*) const;
};
std::unique_ptr<implementation, deleter> _remote; // remote part
public:
constexpr my_type() noexcept = default; // <--
my_type(int x, int y);
~my_type() = default;
my_type(const my_type&);
my_type& operator=(const my_type&);
my_type(my_type&&) noexcept = default;
my_type& operator=(my_type&&) = default;
friend bool operator==(const my_type&, const my_type&);
friend bool operator!=(const my_type& a, const my_type& b) { return !(a == b); }
};
} // namespace v44
In general we want the minimum number of public calls with private access to provide a type which is:
Other operations should be implemented in terms of those
A basis is expressive if it allows compact and convenient definitions of procedures on the type.
operator<()
we don't need the other comparisons:(a > b) == (b < a)
(a <= b) == !(b < a)
(a >= b) == !(a < b)
(a == b) == !(a < b || b < a)
(a != b) == (a < b || b < a)
if (!(a < b || b < a)) some_operation();
is not as expressive as:
if (a == b) some_operation();
Where we have standard operators or other strong conventions, supply those operations
Supply any other operations that are likely to be common
This still leaves a fair amount up to the designer to choose how to balance safety and efficiency and what expressive means in the context of the type
operator&()
, don't.std::addressof()
.std::hash<>
for your typehash_combine()
function or a tuple hashstd::tie()
to easily provide a hash functionoperator<<()
for ostream is useful for debuggingnamespace v45 {
class my_type {
struct implementation; // forward declaration
struct deleter {
void operator()(implementation*) const;
};
std::unique_ptr<implementation, deleter> _remote; // remote part
public:
constexpr my_type() noexcept = default;
my_type(int x, int y);
~my_type() = default;
my_type(const my_type&);
my_type& operator=(const my_type&);
my_type(my_type&&) noexcept = default;
my_type& operator=(my_type&&) = default;
friend bool operator==(const my_type&, const my_type&);
friend bool operator!=(const my_type& a, const my_type& b) { return !(a == b); }
friend std::ostream& operator<<(std::ostream&, const my_type&);
};
} // namespace v44
namespace v45 {
ostream& operator<<(ostream& out, const my_type& a) {
const auto& self{*a._remote};
return out << "{ \"x\": " << self._x << ", \"y\": " << self._y << " }";
}
} // namespace v45
{
using namespace v45;
my_type a{10, 42};
cout << a << "\n";
}