Brushup January 2022
Item 17: Understand special member funcion generation
- Special functions: constructor; destructor; copy constuctor; copy assignment; move constructor; move constructor.
- General rule: A default form is generated automatically by the compiler if called;
the default functions are public and inline; the default functions reverberate
to member data and base classes if needed; if one of the special member
functions is defined, the default version for this function is not generated;
the explicit definition of one special function does not affect the automatic
generation of the others (this is deprecated in C++11. It still works, but
probably won't in future versions. It is also true if a destructor is explicitly
declared. Use
=default). - Special rule for move (1): the move member functions are not independent. That is, if the move constructor is defined, there is no way that the compile will generates the default version for the move assignment.
- Special rule for move (2): No default move operation is generated if any copy operation is explicitly defined.
- Special rule for move (3): C++11 does not generate default move operations if a destructor is explicitly defined.
- The rule of three: It is a good practice to: whenever define one of the copy constructor; copy assignment; or destructor, to define all the three. That is because it is likely the case that a special implementation for any of these likely implies an special implementation for the others.
- The rule of five:
Common case for base classes
class base {
public:
virtual ~Base() = default;
Base() = default;
Base(const Base&) = default;
Base& operator=(const Base&) = default;
Base(Base&&) = default;
Base& operator=(Base&&) = default;
CPP-Conf-20. Back to Basics: Design Patterns - Mike Shah
- Gang of four book: Design patterns: https://en.wikipedia.org/wiki/Design_Patterns
- Design pattern in games book: https://gameprogrammingpatterns.com/
- Three categories: Creational; Structural; Behavioural.
- Singleton: The example given for using the Singleton pattern was not very good. The same could have been accomplished by setting constants in a namespace.
- Singleton: I believe that the general case for this pattern is the following. If you need some sort of initialization before consuming members and this initialization should happen just once, then you have the case for a Singleton pattern.
- Adapter pattern: It is like a wrapper. It is used to create a new interface from some other well stablished interface. Maybe you want to hide or to extend some functionalities.
- Iterator pattern: This one is recommended for lazy containers; or if you plain to have multiple types of containers.
CPP-Conf-20. Back to Basics: Templates Parts 1 and 2 - Andreas Fertig
Item 15: Use constexpr whenever possible
constexprin objects: Signal that its values are constant and known at compilation time.constexprfunctions: If all the arguments of this function are known at compilation time, then the return value of this function is computed at compilation time! If there is at least one of the arguments that is not known at compilation time, then it behaves as a normal function and the return value is computed at runtime.- In C++11,
constexprfunctions can contain only a single executable statement. That is, a return statement. - In C++14, this constraint is relaxed. The only restriction there (which is also
a restriction in C++11) is that you are limited to return
literal types, that is, types that have their values determined during compilation. - You can have user-defined
literal typesbecause constructors and other member function can be declaredconstexpr. - Nice example (considering that constuctor and getters of Point are
constexpr. - Having the ability to do some computation during compilation time is of particularly importance for programmers of embedding systems.
- In C++14, also the setter member functions can be declareed
constexpr. They could not in C++11 because there we have thatconstexprfunctions were implicitly const; and also thatvoid(the return type of a setter) is not considered aliteral type.
Item 16: Make const member functions thread safe
mutablekeyword: This keyword is use to classify data members that can have this value updated even if this happens inside aconst functionor if the object that contains is was declared asconst.- A
std::mutexis a move-only type. - Because of
mutablewe may haveconstfunctions that are actually not thread-safe, and this could lead to problems. That's the reason behind this item.
CPP-Conf-20. Collaborative C++ development with VSCode
Item 41: Consider pass by value for copyable parameters that are cheap to move and always copied
- This, by no means, overrules the wisdown that you should prefer to pass arguments by reference.
- For copyable, cheap-to-move arguments that are always copied, pass by value may be nearly as efficient as pass by reference. The advantage is that you may have to write just one function, therefore, less object code and less code to maintain.
Item 42: Consider emplacement instead of insertion
Fundamental example
The string literal is used as the argument in the constructor of the element being included in the vector. In the push-back version, a temporary object is created (such that it can be passed as a rvalue-reference). Then, inside push_back, is used again to construct the object that will go inside the container. emplace_back, on the other hand, uses perfect forwarding.
Difference between copy initialization and direct initialization.
MyClass{
MyClass(std::string s)
};
MyClass x = "abc"; //copy initialization
MyClass x("abc"); //direct initialization
If the constructor was explicit, the copy initialization would not be allowed. The explicit prevents implicit conversions, that are done in copy initialization.
Item 21: Prefer std::make_unique and std::make_shared to direct use of new
Using new potentially lead to memory leak.
How? You may ask. This is a compiler-related issue. Before calling a function,
the arguments must be resolved. It could be the case that the compiler decides
to execute the code in the following order:
1. new Widget()
2. computePriority()
3. std::shared_ptr
If computePriority throws an exception, we have a leak because the dynamicaly allocated pointer created in the first instruction was not wrapped into a smart pointer.
Sometimes new is necessary
There are some edge cases in which the use of new may be necessary. For example, if you need to pass a custom deleter. In this case, simply create the smart pointer in an isolated instruction.
auto sp = std::shared_ptr<Widget>(new Widget(), customDeleter);
process(sp,computePriority);
process(std::move(sp),computePriority); //even better!
CPP-Conf-20. Template Metaprogramming: Type traits
- Mandatory video: Cpp-Con 2014 Template tutorial by Walter E. Brown.
Metafunctions
Metafunctions are class templates which are supposed to return a value. Such a value is by convention stored as a static const expr member called value, if we are talking about value metafunctions, or in a type called type, if we are talking about type metafunctions.
C++17 feature
In C++17 we have auto non-type template parameters
template<auto X> //before template<typename T, T X>
struct ValueIdentity{
static constexpr auto value = X;
};
Community convetions
Convention with the advent of Variable Templates
We have a similar convention for type metafunctions. As a bonus, typename dance is removed.
Conversion operator
operator int() const { return 7; } // implicit conversion to int
explicit int*() const { return nullptr: } // explicit conversion to int*
Basic metafunctions provided by the standard library
- Cpp17UnaryTypeTrait: must inherit from
integral_constant - Cpp17BinaryTypeTrait: must inherit from
integral_constant -
Cpp17TransformationTrait
-
integral_constant
template class<class T,T v>
struct integral_constant{
static constexpr T value = v;
using value_type = T;
using type = integral_constant<T,v>;
constexpr operator value_type() const noexcept{
return value;
}
constexpr value_type operator()() const noexcept{
return value;
}
template class<bool B>
using bool_constant = integral_constant<bool,B>
using true_type = bool_constant<true>;
using false_type = bool_constant<false>;
is_voidmetafunction via specialization.
template<typename T>
struct is_void : std::false_type {}
template<>
struct is_void<void> : std::true_type {}
static_assert( is_void<int>{} ) //Internally, it calls the implict conversion operator above of integral_constant
remove_const(Transformation trait)
remove_const<int> -> int
remove_const<const int> -> int
remove_const<const volatile> -> volatile int
remove_const<int *> -> int *
remove_const<const int *> -> const int *
remove_const<const int * const> -> const int *
Primary type categories
Primary type categories and templates: For any given type T, the result of
applying one of these templates to T and to cv T (const/volatile) shall yield
the same result. The type void is in this category.
- is_void
- is_null_pointer
- is_integral
- is_floating_point
- is_array
- is_enum
- is_union
- is_class
- is_function
- is_pointer
- is_lvalue_reference
- is_rvalue_reference
- is_member_object_pointer
- is_member_function_pointer
A word from the standard: You should never implement yourself specialization
of these types. One thing more: For any given type T, exactly one of the primary
type categories shall yield true_type.
Word about templates
Full specialization:
Partial specialization
template <typename T>
struct MyClass<T const> // partially defined (const qualifier). I am using east const notation here.
Conditional type metafunctions
Conditional type metafunction. Return a type based on a condition.
The decltype functioning
It is like telling the compiler: Pretend you call this function. What type does it return? (But don't actually call it).
What about if the function I am calling has parameters? Use declval
The declval guarantees you are in a context free of evaluation.
Member pointer type
int* is a valid pointer type. Does not have to point to anything
SFINAE: Substitution error is not a failure
template <typename T>
std::true_type can_have_pointer_to_member(int T::*);
template <typename T>
std::false_type can_have_pointer_to_member(...);
static_assert(decltype(can_have_pointer_to_member<int>(nullptr))); //Compiler error
template <typename T>
std::true_type can_have_pointer_to_member(int T::*);
template <typename T>
std::false_type can_have_pointer_to_member(...);
template <typename T>
using can_have_member_ptr = decltype(can_have_pointer_to_member<T>(nullptr));
static_assert(decltype(can_have_member_ptr<int>(nullptr))); //SFINAE
Rule of thum: Use SFINAE in the return type whenever it is possible.
Item 22: When using the Pimpl idiom, define special member functions in the implementation file
Why Pimpl idiom?
One of the advantages of the Pimpl idiom is to save compilation time for the clients of the class.
Let us say that our class Widget has a dependency on class Gadget. That is,
Gadget is a member of Widget. Including the header Gadget.h in Widgets.h
will make the compiler to compile Widgets again every time Gadgetis updated.
Using the Pimpl idiom, we wrap all its members to a Widget::Impl class, which
is declared as an incomplete type in Gadget. Next, we define Widget::Impl
in Widget.cpp
//widget.h
struct Widget{
struct Impl;
std::unique_ptr<Impl> pImpl;
~Widget();
Widget(Widget&&);
Widget& operator=(Widget&&);
Widget(const Widget&);
Widget& operator=(const Widget&);
};
//widget.cpp
#include "widget.h"
#include "gadget.h"
Widget::Impl{
Gadget g;
}
Widget::Widget() : pImpl(std::make_unique<Widget::Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) = default;
Widget::operator=(Widget&&) = default;
Widget::Widget(const Widget& other) : pImpl(std::make_unique<Widget::Impl>(*other.pImpl)){}
Widget::operator=(const Widget& other){
*pImpl = *other.pImpl;
return *this;
}
Notice that we declare the delete in the header but we defined it in the cpp.
That's because the compiler would generate an error otherwise. The automatic generated
destructor is generated inline, in the header file, and at that point Impl is not
a complete type yet. The destructor of the unique_ptr will try do delete an incomplete
type, and that is not allowed.
Because whenever we specify that we are not using the default delete function, the
default move operations are not aumatically generated as well, and that's why we
declare and define them ourselves.
Finally, the copy member functions should be specifically declared, otherwise we would
have a shallow copy instead of a deep copy of pImpl.
Item 23: Understand std::move and std::forward
What is a move and what is a forwarding
std::move performs an unconditional cast to an rvalue object.
std::forward casts to an rvalue object if the object (created as an input parameter)
was passed as an rvalue.
Ephemeral characte of a rvalue
The rvalue character of an object is ephemeral. In a function definition, an rvalue parameter becomes an lvalue parameter in the scope of the function (you need to store the passed parameter somewhere, and every time you store, it is a lvalue).
We use std::forward to recover the rvalue character of the passed parameter (if any)
inside if the function scope.
Move and const qualifier
Be aware that move constructors and move operators are not const qualified. That is
quite natural, because these functions are supposed to change these objects passed as
parameters, so they can't be const qualified.
As a rule of thumb: Never move const objects. You may got a behaviour that is not the expected one. For example, to have the object being copied, instead of being moved.
class MyClass{
std::string m_text;
MyClass(const std::string text) :
m_text( std::move(text) ) {} //Does not move. It copies!
MyClass(std::string text) :
m_text( std::move(text) ) {} //This moves, indeed.
}
Cpp-conf-21: B2B: Compiling and linking
Pre-processing
- Headers + Cpp -> PreProcessor -> Translation Unit per .cpp (.i) -> Object file (.o) -> Linking -> Excecutable
- Object file: "Data" and "MetaData"
- Metadata:
- "Text" or "Code": for machine instructions
- "Literal": initialized read-only data
- "Data": initialized read-write data
- "Bss": uninitialized read-write data.
- When creating the object file, some placeholders that refer to other translation units are created. It is the job of the linker to resolve these placeholders by linking them to the actual place of definition.
One definition rule
- One Definition Rule: No translation unit may define certain entities more than once.
- A translation unit is what we've got after preprocessing and including all the headers text in the same file as the source code.
- Some entities must be defined in exactly one translation unit:
- non-inline variables
- non-inline, non-template functions
constexprandconstevalfunctions are implicitly inline- It is easier to write a macro that will misbehave than an inline function that will misbehave.
- C++ Modules perspective is to decreasing build times by its new code organization structure, which is supposed to increase build time efficiency.
The special case of templates
Templates are usually declared and defined in header files. But remember, templates are recipes for class and function generation, not real classes and functions.
An explicit template specialization is, on the other hand, not a recipe, but an existing entity representing a class, a function or a variable. So the ODR rules apply here. That's it, we have the definition of an explicit template specialization separated from its declaration.
- Explicit Template Instantiation
//file: a.h
//function template definition
template <typename T>
void swap(T &x, T &y){
T temp(std::move(x));
x = std::move(y);
y = std::move(temp);
}
// explicit function instantiation declaration
extern template
void swap<int>(int &x, int &y);
//file: a.cpp
//explicit function instantiation definition
template
void swap<int>(int &x,int &y);
Notice that we don't provide a body in the explicit function instantiation.
The compiler will use the body in the template declaration. This technique is
used to instruct the compiler to generate the version for ints of the function
template swap in the source file a.cpp and not somewhere else. It also instructs
the compiler that this should be unique and sole definition for this version to use.
Remember that the compiler might generate the code for a function template several times during the compilation. But these generated code have special metadata instructing the compiler to consider just one of these several definition (pick any flag).
In this sense, entity templates behave as inline functions. They are pasted in several tanslation units and the compiler rids off all of them except one.
Item 24: Distinguish universal references from rvalue references
- Universal references appear in the context of type deduction. That is, when
templates or the
autokeyword is involved. - Universal references can bind to a rvalue reference but also to a lvalue
reference. It can bind to
constorvolatilevariations of them either. - To be defined as a universal reference, we must have something similar to
Anything else discards the possibility of param to behave as an universal reference.
That includes cv qualifiers such as const and volatile. When they are present,
param is an rvalue for sure.
void f(Widget&& param); // rvalue reference
Widget&& var1 = Widget(); // rvalue reference
auto&& var2 = var1; // Universal reference
template<typename T>
void f(std::vector<T>&& param); // rvalue reference
template<typename T>
void f(T&& param); // Universal reference
template<typename T>
void f(const T&& param) // rvalue reference
The rule of thumb is: If there is type deduction involved and the param strictly
respects the format T&&, then it is a universal reference.
Cpp-Conf-20. C++20 STL Features.
String views
static constexpr array skipped_extensions{".dll"sv, ".exe"sv, ".obj"sv};
static_assert(ranges::is_sortes(skipped_extensions));
erase-remove idiom
- In C++20 we have uniform container erasure =D. Simply calling
erase_if(v,pred)will work as expected.
C++20 New features
atomic_ref: think about a variable that you realize several non-atomic operations, but at some point, you need to do atomic operations on it. Useatomic_refin this part.span: It is like astringview. A pointer with length. Is it readonly? It is supposed to be used for contiguous containers such asvector,listandarrays.- read more about ranges. C++20
ranges::contiguous_range. - read more about
string view - GH-724: Fix boyer_moore_searcher: Fixed a 43-year-old bug by implementing a 40-year-old fix.
Item 25: Use std::move on rvalue references and std::forward on universal references
Rule of thumb
Always move a rvalue and always forward a universal reference. A rvalue
parameter (Widget&& w) is always bound to rvalues, so you should unconditionally
cast it to rvalue to preserve its character. Universal references, on the other
hand, may have a rvalue character, but may be not. In this case, use forward
to preserve its original character and eventually cast it in to a rvalue.
String literals and universal references
//Implementation 1
class Widget {
public:
template<typename T>
void setName(T&& newName) //Universal reference
{ name = std::forward(newName); }
private:
std::string name;
}
//Implementation 2
class Widget {
public:
void setName(const std::string& newName)
{ name = newName; }
void setName(std::string&& newName)
{ name = std::move(newName); }
private:
std::string name;
}
Widget w;
w.setName("Amazonia");
Let's consider the code generated with respect the two implementations.
In the first, a string literal "Amazonia" is passed to the template member function
setName. Notice the member function takes an universal reference, that's why we
are forwarding.
The deducted type is: const char(&)[9] (or const char*). A string literal is a lvalue, therefore,
forward forwards a lvalue. The string literal is directly used as a parameter
of the assignment operator.
In the second implementation, the string literal is converted to a string type. Therefore, a temporary string object is created. Then, this same object is passed as a parameter to the assignment operator. In this implementation we spend one string constructor that was not necessary in the first.
// These copy from ROM to RAM at run-time:
char myString[] = "hello";
const int myInt = 42;
float myFloats[] = { 3.1, 4.1, 5.9 };
// These copy a pointer to some data in ROM at run-time:
const char *myString2 = "hello";
const float *myFloats2 = { 3.1, 4.1, 5.9 };
Return Value Optimization (RVO)
The standard says: The compilers may elide the copying (or moving) of a local object (function parameters are not included) in a function that returns by value if (1) the type of the local object is the same as that returned by the function and (2) the local object is what's being returned.
We hinder RVO when writing return std::move(w) on a local variable w because
the return type is not anymore the same of the local variable. It is a reference
to w. And because of that, RVO should not be applied.
In other words, it may make sense to return forward or return move a function
parameter. But doing that in a function local variable hinders RVO. Indeed, compilers
are constrained to elide the copy or to apply a move in the return value if the
conditions for RVO are met.
Even when the RVO is not met, for example
This function is not eligible for copy elision (w is a function parameter), but
the compiler must treat it as if it was written as:
The lesson is: You can't help your compiler by applying a std::move in a return
statement involving a local variable but you can certainly hinder them
(by precluding the RVO).
- Read more about RVO and its conditions.
Item 26: Avoid Overloading on universal references
Function taking universal references are the optimal choice sometimes
void logAndAdd(const std::string& name)
{
auto now = std::chrono::system_clock::now();
log(now,"logAndAdd");
names.emplace(name);
}
std::string petName("Darla");
logAndAdd(petName); //emplaces receives a lvalue, thus it copies.
logAndAdd(std::string("Persephone")); //the rvalue is bounded to the parameter, a lvalue, thus emplaces copies.
logAndAdd("Patty Dog"); //the string literal is used to create the string parameter which is also again copied in emplace.
template<typename T>
void logAndAdd(T&& name)
{
auto now = std::chrono::system_clock::now();
log(now,"logAndAdd");
names.emplace(std::forward<T>(name));
}
std::string petName("Darla");
logAndAdd(petName); //emplaces receives a lvalue, thus it copies.
logAndAdd(std::string("Persephone")); //The parameter is a rvalue, thus emplaces moves.
logAndAdd("Patty Dog"); //the parameter is a string literal. Emplaces uses directly the string literal in the string constructor.
Problems start to arise when we try to overload a function that is already taking universal parameters. The reason is that functions taking universal references are the greediest functions in C++.
void logAndAdd(int id)
{
auto now = std::chrono::system_clock::now();
log(now,"logAndAdd");
names.emplace(names_collection[id]);
}
short id = 42;
logAndAdd(42); //Since it is not an int, it calls the universal reference version.
Overload resolution rules
- An exact match beats a match with a promotion (short to int).
- The rule above also applies to keuword qualifiers such as const.
The problem caused by overloading a universal reference function is particularly problematic with perfect forwarding constructors.
class Person
{
template<typename T>
Person(T&& name) : name(std::forward<T>(name)) {}
}
Person p("Daniel");
Person q(p); //Calls the perfect-forwarding constructor instead of copy constructor.
//because `Person(Person& name)` is a better match than `Person(const Person& name)`.
Cpp-Conf-20: Modern C++ Safety and Security
- Hard, firm and soft real time requirements. They are defined according to its usefulness level decay against the latencey passed a deadline time.
- I've never have seen
exceptionsbeing used in the embedded context. They are expensive, mainly because of stack uniwinding. But also, if an unhandled exception is thrown, the program goes to the top of the stack and resets the program state. - Memory allocation and deallocation are not done in system with hard real time requirements. All memory has already been allocated priorly.
C++20 Features
- Concepts
- Ranges
- Coroutines
- Modules
- std::format replaces printf
- constexpr++
- using enum
- volatile deprecation
- designated initializers (used in aggregates "structs without constructors").
- spaceship operator
- calendar + time zones
- string literals
- span
About interfaces
Design it to be easier to use correctly and harder to be used incorectly. What does this say about optional parameters and interface change / extensions?
Concepts
With concepts, we now may have several destructors
constexpr ~Optional() requires (!std::is_trivially_destructible_v<T>) { ... }
constexpr ~Optional() = default;
Ranges
We thought that iterators were safe until we discover they are not (examples?). Ranges solve that.
//it seems that we should be very careful while using
//the ranges library because it may lead to very unefficient code... though pretty
//the | operator is very expensive!
views::filter(bit_1_mask) | views::reverse | views::transform([])(int n){ return n*3; }
- It would be nice to follow a didactic video on C++20 features.
- What about trying to define my function with pre and post conditions. Contracts will allow to do that in C++23.
- Why people don't like exceptions?
- Exploiting Modern C++ (Book)
Item 27: Familiarize yourself with alternatives to overloading on universal references
Why use Universal references?
- We want to take advantage of universal references being binded sometimes to lvalues and sometimes to rvalues. The conditional casting performed by perfect-forwarind allows us to write more concise functions.
- Sometimes is more efficient. Using
const T&does not allow us to use move semantics.
What are the alternatives?
- Abandon Overloading. That is, define a function with a different name. This is not really an alternative if you are handling constructors, though.
- Passing by
const T&. Your code won't be the most efficient implementation in some occasions. Namely those that involves move semantics. - Pass by value. If you need at some point to copy the parameter anyways, pass by value may be the right choice. You can safely use move semantics on it.
- Tag Dispatch. This technique consists in create a compile time condition. You can
maintain the universal reference, but now you have an additional parameter. You
then implement one version for each variation of this second parameter. Scott
used
std::false_typeandstd::true_typefor example.
To implement TagDispatch you need a dispatcher function and one or more implementations. Possibly overloaded, being the TagDispatch the way to instruct the compiler to choose the right one. - Template enablement. This one consists to use
std::enable_ifand what is known asSFINAE(Substitution Failure Is Not An Error).
Definition of SFINAE
If an invalid argument or return type is formed during the instantiation of a function template, the instantiation is removed from the overload resolution set instead of causing a compilation error.
What is considered an invalid argument or an invalid return type? - A function with no return type specification is invalid. All functions must define a return type.
With respect to function templates, enable_if can be used in multiple different ways:
- As the return type of an instantiatied function;
- As an extra parameter of an instantiated function;
- As an extra template parameter (useful only in a compiler that supports C++0x default arguments for function template parameters, see Enabling function templates in C++0x for details.
Implementation of enable_if in boost
template <bool B, class T = void>
struct enable_if_c {
typedef T type;
};
template <class T>
struct enable_if_c<false, T> {};
template <class Cond, class T = void>
struct enable_if : public enable_if_c<Cond::value, T> {};
Utility template function
- The types
intandint&are not the same! Think of usingstd::remove_reference. std::enable_if,std::is_base_of,std::is_constructible,static assert,std::decay
Item 28: Understand reference collapsing
- You are not allowed to declare reference to reference variables. That is, the following code is forbidden.
- Nonetheless, the compiler is allowed to do so. When the character of a parameter
passed to a universal reference parameter is a lvalue, the deduced type is
T&. In other words, we have
//the function template
template<typename T>
void myFunction(T&&){ }
//generates
void myFunction(Widget& &&){ }
//when there is one invokation passing a lvalue reference
Widget w;
myFunction(w);
In these cases, the compiler generates a reference to reference and just after executes a reference collapsing.
- Rule for reference collapsing: If either reference is an lvalue reference, the result is an lvalue reference. Otherwise (i.e., if both are rvalue references) the result is an rvalue reference.
Wherever there is a Universal reference, the following will happen:
- Lvalues of type
Tare deduced to have typeT&, while rvalues of typeTyieldTas their deduced type; - Reference collapsing occurs.
auto&& w1 = w;
//is deduced as
Widget& && w1 = w;
//after reference collapsing
Widget& w1 = w;
//On the other hand
auto&& w1 = widgetFactory();
//is deduced as
Widget&& w1 = widgetFactorty;
- Reference collapsing also happens during typedefs
template<typename T>
class Widget {
public:
typedef T&& RvalueRefToT;
...
};
Widget<int&> w;
//is deduced as
typedef int& && RvalueRefToT;
//reference collapsing occurs and thus
typedef int& RvalueRefToT; //We have a lvalue instead of rvalue.
- Besides template, auto and typedef instantiation, reference collapsing also occurs in decltype.
Reference collapsing is in the realm of why std::forward works, that is, why std::forward returns lvalue if the template character is a lvalue or a rvalue if the character is a rvalue. Here is the implementation of std::forward
template<typename T>
T&& forward(typename remove_reference<T>::type& param){
return static_cast<T&&>(param);
}
Here it is an example using universal references and forward:
CPP-Conf-20: C++20 Ranges In Practice
std::ranges::dangling protection
//till C++17, this would return a dangling iterator
//in C++20, it returns a std::range::dangling. It is basically
//a empty struct, but the compiler now shout out an error if we
//try to effectuate an operation on it. Good!
auto iter = std::ranges::min_element(get_vector());
The std::ranges::dangling is returned every time a rvalue is passed to a
function of the ranges library. However, some types are safe to pass by rvalue.
For example, string_view and span. We can disable this behaviour for our
specific type by specializing enabled_borrowed_range.
Views
A view is a range which is - default constructible - has constant-time move and destruction operations - (if it is copyable) has contant-time copy operations
//views are lazy sometimes
auto view = std::views::transform(vec,[](int i){ return i*i; })
auto sumsq = std::accumulate(view.begin(),view.end(),0);
- What is
std::ranges::view::common?
std::invoke
It calls a callable object with a list of arguments. The callable object
could be a function object, a pointer to member function or a pointer to
data member.
- pointer to member function: INVOKE(f, t1, t2, ..., tN) is equivalent to (t1.*f)(t2, ..., tN)
- pointer to data member: INVOKE(f, t1) is equivalent to t1.*f
- function object: INVOKE(f, t1, t2, ..., tN) is equivalent to f(t1, t2, ..., tN)
std::projection
Views and ranges adapter
std::views::drop_whilestd::views::reverse- Adapters composition to create new adapters
Examples
template<typename R>
auto trim_back(R&& rng){
return std::forward<R>(rng)
| views::reverse
| views::drop_while(::isspace)
| views::reverse;
}
auto trim_back(){
return views::reverse
| views::drop_while(::isspace)
| views::reverse;
}
auto trim_front(){
return views::drop_while(::isspace);
}
//I can transform this in an inline constexpr variable! Same for the two above
auto trim(){
return trim_front() | trim_back();
}
std::string trim_str(const std::string& str){
return string_view(str)
| trim()
| rangesnext::to<std::string>; //external library
}
Item 29: Assume that move operations are not present, not cheap, and not used
Scenarios
- No move operation.
- Move not faster than copy. Moving an array is tantamount copying an array. Or copy for smal strings (Small String Optimization, SSO). String with fewer than 16 characters are stored in an internal buffer and there is no memory allocation involved.
- Move not usable. If not declared as noexcept and the function supposed to use the move semantics imposes strong exception safety guarantee.
- Source object is
lvalue. In some situations, it is ok to move lvalues. For example, copy-by-value function parameters which all modifying operations have been done prior to the move operation. But in general, move operations are supposed to be done in rvalues.
In my opinion, these reasons state the opposite, actually. They say that move semantics may not be as efficient as expected, but in these cases they are going to be as efficient as copy operations. Therefore, it is better to favor move operations because in the worst case, you are going to get the runtime of a copy, which is the alternative to move operations.
Item 30: Familiarize yourself with perfect forwarding failure cases
Braced initializers
Consider the following scenarios:
void f(std::vector<int> v){ ... }
template<typename T>
void fwd(T&& v){
f(std::forward<T>(v));
}
f({1,2,3}); //Call 1
fwd({1,2,3}); //Call 2
In the first call, the compiler 'sees' that the type should be deduced to
std::vector<int> and implicit conversion applies. But in the second, the
compiler has no prior information about the type to be deduced at the call site,
that is, fwd({1,2,3}).
According to the standard, std::initializer_list is a "non-deduced context".
That means that compilers are forbidden from deducing a type for the expression
{1,2,3} in the call to fwd.
Workaround:
auto il = {1,2,3}; //For auto, compilers are allowed to deduce this as being an `std::initializer_list`
fwd(il); //That works
0 or NULL
This is clear. The way to go is to use nullptr.
Declaration-only integral static const data members
Generally, you don't need to define integral static const data member. In most
situations you are set by simply declaring it.
As long as you don't create a pointer to VAL, everything will work fine.
The compile will plop the value 42 every time it finds C::VAL. But if you
work somehow with a pointer to VAL then an error will occur, because in this
case you will need a definition.
It happens that when forwarding integral static const members we have the same problem, and we need to define the integral member.
Overloaded function names and template names
Consider this
int processFunction(int a);
int processFuntion(int a,int b);
void f(int (*pf)(int));
template<typename T>
void fwd(T&& v){
f(std::forward<T>(v));
}
f(processFunction); //call 1
fwd(processFunction); //call 2
In the first call, everything goes fine. The compiler chooses the correct
overload of processFunction. In call 2, by a reason similar to the first topic in
this item, the compiler does not know which overloaded function to pass at the
call site of call 2.
Once more, the workaround is to define a variable taking the type of the correct
overload function and then pass this variable to fwd.
Bitfields
We declare bitfields in C++ as the following
Because bitfields do not necessarily begin at the beginning of a byte, address of a bitfield can't be taken. Pointers and non-const references to bitfields are not possible. When initializing a const reference from a bitfield, a temporary is created (its type is the type of the bitfield), copy initialized with the value of the bitfield, and the reference is bound to that temporary.
The reason why bitfields cannot be forwarded lies in the fact that we cannot have pointers to them (remember that, internally, pointers and references are similar). We need first to bound the bitfield to a variable in order to forward it.
Item 31: Avoid default capture modes
There are two default capture modes.
- By reference: [&]
- By value: [=]
These modes translate to: Make available in the lamba context, everything that is available in the context where the lambda was created.
The main precaution to be take here is to ensure that the lifetime of the captured values outlives the lifetime of the lambda function. Having saying that, it is a good practice to be explicit about which variables the lambda context will capture instead of using default modes. The rational is that by expliciting listing the variables, we are more likely to remember that those must outlive the lambda.
One could think that the problem described above it is only sensible to [&].
That is, that the problem occurs only if the captured variable is destroyed
before a call to the lambda (which would create a dangling reference to the
captured variable). But we may face a similar issue using the by value default
mode [=].
std::vector< bool(int*) > filters;
class Widget{
public:
void addFilter() const;
private:
int divisor;
};
void Widget::addFilter() const{
filters.emplace_back(
[=](int value){ return value % divisor == 0; }
);
}
capture all by value
mode, nothing is being captured there. Lambda contexts are only allowed to capture
non-static local variables. Here, divisor is a member of Widget and not a local
variable in the context of addFilter.
Despite that, this code compiles. That's because the variable this is implicitly
captured, and divisor is implicitly substituted to this->divisor. As you know,
this is a pointer, and a copy by value won't save you from dangling country if
the lambda function outlives the Widget which created it.
Here it is another example:
void addDivisorFilter(){
static auto calc1 = computeSomeValue1();
static auto calc2 = computeSomeValue2();
static auto divisor = computeDivisor(calc1,calc2);
filters.emplace_back(
[=](int value){ return value % divisor == 0; }
);
++divisor;
}
Once again, nothing is being captured here. Lambda contexts do not capture
static variables. The code above compiles because it is referring directly to
the stati divisor variable. In fact, it is like divisor is being passed by
reference. We have no clusure at all in this case as well. Every time we invoke
the created lambda, the value of divisor may had changed.
Item 32: Use init capture to move objects into closures
In C++14, the C++11 capture mechanism is super seeded by what is called the generalized capture. Here it is an example
auto pw = std::make_unique_ptr<Widget>();
//...configure pw
[pw = std::move(pw)](){
// do some stuff with pw
}()
= we are in the scope of the lambda class (the class that every lambda generates
to encapsulate the capture object); at the right of = we are in the scope where the
lambda was created.
In C++11, it is not possible to move-construct an object into a closure, as it is possible with the generalized capture mechanism in C++14, but there is a workaround to it.
Cpp-Conf-20: OO Considered Harmful
References:
1. GO TO Statement considered harmful. Dijkstra.
There is an argument about how our mind poorly perceives time.
Go To would interfer in a linear structure of a program.
2. The Forgotten Art of Structured Programming.
3. Squaring the circle.
Brief story of OO. Language Hierarchy
- SmallTalk
Algol - Simula / - Objective C - C -\ - C++
Message passing (dynamic binding) x Virtual dispatch in C++ (Virtual tables)
What are the costs of Virtual Tables? (Heap allocation?)
What is Type Erasure? (e.g. Dyno)
Look at std::visit
Polymorphism: It is really necessary? Single inheritance isn't enough? Sure we can have the same effect with single inheritance, but polymorphism simplify stuff. Encapsulation: Do we need OO to have encapsulation (think abount const public members)
Cpp-Conf-20: C++20: An almost complete overview
References: Professional C++. Marc Gregoire
- Modules
- Ranges
- Coroutines
- Concepts
Modules
- Will replace header files in the long run.
- Code declared in the interface (header) won't be exported. That means that a change in the interface body won't trigger a rebuild everywhere the interface is needed.
- No need for include guards
- Example of a module:
// Create a Module
// ccpcon.cppm - Module Interface File
export module cppcon; // Module declaration
namespace CppCon{
auto GetWelcomeHelpert() { return "Welcome to CppCon 20020!"; }
export auto GetWelcome() { return GetWelComeHelper(); }
}
// Consume a Module
// main.cpp
import cppcon;
int main(){
std::cout << CppCon::GetWelcome();
}
import <cstdio>. It will export everything in the interface. When
importing a C++ header file, also the macros are exported. Normally, macros are
not exported.
Ranges
How it works the lazy characteristic of Ranges?
Range: A concept defining iteration requirements. Any container supporting begin/end is a valid range.
Range-based algorithms: All STL algorithms accepting ranges instead of iterator pairs.
Projection: Transform elements before handing over to algorithm.
Views: Transform/filter range: lazily evaluates, non-owning, non-mutating
Range factories: construct views to produce values on demand
Pipelining: Views can be chained using pipes, e.g |
What is the difference between a view and a projection?
Example with infinite sequences:
auto result { view::ints(10) //infinite list of integers starting at 10
| views::filter([](const auto& value) { return value % 2 == 0; })
/* ... */
| views::take(10) //Takes first 10
};
Coroutines
Those that have co_yield, co_await.
It is useful to create generators and do lazy computation.
Concepts
// Concept definition
template<typename T>
concept Incrementable = requires(T x){ x++; ++x; }
// Using the concept
template<Incrementable T>
void Foo(T t);
template<typename T> requires Incrementable<T>
void Foo(T t);
template<typename T>
void Foo(T t) requires Incrementable<T>;
void Foo(Incrementable auto t);
// Another example
template<typename T>
concept C = requires (T& x, T& y) {
{ x.swap(y) } noexcept;
{ x.size() } -> std::convertible_to<std::size_t>;
...
};
Lambda expressions
- Default by value capture does not implicitly includes this anymore.
- We now have template lambda expressions.
constexpr
- virtual functions can be constexpr.
- dynamic_cast and typeid is also allowed
- heap allocation is also allowed
- string and vector are now constexpr!!!!
atomic smart pointers
- we have a type atomic_shared_ptr
jthread
- it automatically cancels thread and calls join() if the jthread object is destroyed before join or detach been called.
synchronization primitives
- we have a semaphore class. It generalizes mutexes.
- Other synchronization primitives: Latches & barriers.
misc
- if and switch initialization
-
floating-point types and classes (some restrictions) are accepted as non-type template parameters.
-
Checkout the CTRE library: compile-time string literals to create regular expressions parsers at compile time!!!
span
- read-only span: std::span
using enum directive
std::format
It combines the best of stream operators and printf. There is no reason no to use it
Represents information about a specific location in a source code: line, column, filename, function name
Item 33: Use decltype on auto&& parameters to std::forward them.
In C++14 we have generic lambdas with the auto keyword. It will be clear in the following that this is different from template lambdas that debuted in C++20.
But what about if the parameter x that we pass to our lambda could be
a left or a right value? We should use a universal reference
We don't have access to the deduced type by the auto keyword, so we are not able to pass the template parameter to the forward function, and we must pass it (the reason for that is in how forward work. An explanation is given in item 28). If we rewrite it as
Then decltype resolves to T& if x is a lvalue and resolves to T&& if x is a rvalue.
If we look at the definition of std::forward
and we apply the reference collapsing rules, we are going to obtain exactly what
we want, despites the convention of passing T& when forwarding a lvalue and
T when forwarding a rvalue. Passing T&& as the template parameter for the
forward function also resolver to casting the parameter to a rvalue.
Item 34: Prefer lambdas to std::bind
auto setSoundL = [](Sound s){
using namespace std::chrono;
setAlarm(steady_clock::now() + &h,
s,
30s);
};
auto setSoundB = std::bind(static_cast<SetAlarm3ParamType(setAlarm),
std::bind(std::plus<>(),
steady_clock::now(),
1h),
_1,
30s);
std::bind uses function pointers to invoke the binded function. Compilers have more difficult to inline functions invoked by function pointers. In these situations, lambdas are also faster.