Just like in natural (spoken) languages, C++ coders will probably use idioms from time to time. These are code patterns that perform a specific task, whose function may not be obvious to new C++ coders. Once learnt, however, they can be applied in a variety of settings; a few useful ones are listed here:
1 – Shrink-to-fit
Used with std::string
and std::vector
mostly, the aim is to reduce memory footprint in a running program. It may be thought that a construct such as s.reserve(s.size())
would to this, however member function reserve()
only takes action when increasing the amount of memory reserved for the container. The correct shrink-to-fit construct for a std::string
object named s
would be:
std::string{ s }.swap(s);
Similarly for a std::vector
object named v
(as the element type can be deduced in Modern C++):
std::vector{ v }.swap(v);
This idiom works by creating a temporary variable with the contents of the object to be shrunk, and then swaps this temporary with the initial object (the initial object then being discarded immediately and the heap memory it was using reclaimed). A variant of this idiom swaps with an empty object, typically used after a call to member function clear()
.
2 – Erase-remove
The names of the standard algorithms remove
and remove_if
are slightly misleading; they never remove anything! Instead they operate by swapping all matching elements to the end of the container. A single call to container method erase
then shrinks the container to the correct size, leaving only the desired elements; this is preferred to having multiple calls to erase
in combination with find
/find_if
. (To really release memory, a shrink-to-fit may be necessary, too.)
Both remove
and remove_if
return an iterator at the first element that matched, which is the correct parameter to a call to erase
. (In the case of no match, end()
would be returned which is still a valid value to pass, and which translates to a no-op). The full construct looks like this for a container c
for which any element with value -99
is to be removed (std::
prefixes not shown):
c.erase(remove(begin(c), end(c), -99), end(c));
Similarly for the same container which requires all numbers greater than 100
to be removed:
c.erase(remove_if(begin(c), end(c), [](auto& e){ return e > 100; }), end(c));
This idiom is often written on multiple lines, with suitable indentation to aid comprehension. Note that end(c)
appears twice in each case, as it needs to be specified to both erase
and remove
/remove_if
.
3 – decltype(auto)
It’s been part of Modern C++ so long it no longer feels “new”; auto
is used to query the type of a variable or expression so the coder does not have to worry about “guessing” the correct type for a new variable. In fact decltype()
performs the same task, and is compatible with just about any expression; also it does not strip const
, volatile
and reference (&
, &&
) qualifications, unlike auto
.
There is a corner case for combining the two, related to automatic deduction of function return type; this would usually only be needed with generic functions (using template type parameters). If you see this in code, be aware that it means the same as plain auto
semantically, however the function may be returning a reference value when the return expression is enclosed in parentheses. (This is the most usual case, but this return expression may be qualified with const
and/or volatile
as well).
The following program demonstrates use of decltype(auto)
(instead of using auto&
and auto
respectively):
#include <iostream>
decltype(auto) print = (std::cout); // parentheses are necessary here
decltype(auto) newline = &std::endl<char, std::char_traits<char>>;
int main() {
print << "Hello decltype(auto)!" << newline;
}
4 – Copy and swap
The copy-and-swap idiom is used to provide the strong exception guarantee for some of the special member functions of classes and structs, they are the copy and move assignment operators as well as the move constructor. Consider the following class outline for a “smart” array:
template <typename T>
class Array {
size_t array_size{};
T *array_data{ nullptr };
void swap(Array<T>& other) noexcept;
public:
// other member functions not shown
};
The key thing to note is the private member function swap()
which is declared noexcept
, takes a reference to another Array<T>
and returns void
. This function provides a memberwise swap (using std::swap
) as shown here:
template <typename T>
void Array<T>::swap(Array<T>& other) noexcept {
using std::swap; // scoped using-declaration
swap(array_data, other.array_data);
swap(array_size, other.array_size);
}
As all specializations of std::swap
are noexcept
, this function can also be declared noexcept
. Of course, the actual calls to swap()
would need to be changed based upon the class’s data members. The motivation for creating a swap member function is demonstrated by the correct version of the copy assignment operator for this class:
template <typename T> // copy assignment
Array<T>& Array<T>::operator=(const Array<T>& rhs) {
Array tmp{ rhs };
tmp.swap(*this); // or: swap(tmp);
return *this;
}
Each of the three lines of the funtion body are significant here:
- A copy of the variable
rhs
is created, which uses the (usual) copy constructor (not shown). If this operation throws an exception, no memory is leaked (assuming the copy constructor is well written) and*this
is unchanged. - The member function
swap()
is called ontmp
, making*this
an exact copy ofrhs
, which is the desired outcome. - The “old”
*this
(nowtmp
) goes out of scope, and is deallocated correctly. Remember that destructors are not allowed to throw exceptions, so this too is exception safe.
The move assignment operator is almost identical, the difference being that tmp
is initialized from an r-value reference so std::move
is used. The move constructor is trivial to write and is shown here too:
template <typename T> // move assignment
Array<T>& Array<T>::operator=(Array<T>&& rhs) {
Array tmp{ std::move(rhs) };
tmp.swap(*this);
return *this;
}
template <typename T> // move constructor
Array<T>::Array(Array<T>&& other) {
other.swap(*this);
}
It is also possible to define a free function swap()
taking two references, making this a friend function so that it can perform the memberwise swap. Which variant is chosen is probably a matter of taste.