Upload
wang-hsiangkai
View
196
Download
2
Embed Size (px)
Citation preview
Effective Modern C++ Kai
Template Type Deduction
• The Type deduction for T is dependent not just on the type of expr, but also on the form of ParamType.
• ParamType is a pointer or reference type.
• ParamType is a universal reference.
• ParamType is neither a pointer nor a reference.
template<typename T>void f(ParamType param);
f(expr)
ParamType is a pointer or reference type
1. If expr’s type is a reference, ignore the reference part.
2. Then pattern-match expr’s type against ParamType to determine T.
template<typename T>void f(T& param);
int x = 27;const int cx = x;const int& rx = x;
f(x); // T is int, param’s type is int&f(cx); // T is const int, param’s type is const int&f(rx); // T is const int, param’s type is const int&
ParamType is a universal reference• If expr is an lvalue, both T and ParamType are deduced to be lvalue references.
• It’s the only situation in template type deduction where T is deduced to be a reference.
• ParamType is declared using the syntax for an rvalue reference, its deduced type is an lvalue reference.
• If expr is an rvalue, the Case 1 rules apply.
template<typename T>void f(T&& param);
int x = 27;const int cx = x;const int& rx = x;
f(x); // T is int&, param’s type is int&f(cx); // T is const int&, param’s type is const int&f(rx); // T is const int&, param’s type is const int&f(27); // T is int, param’s type is int&&
ParamType is neither a pointer nor a reference
template<typename T>void f(T param);
int x = 27;const int cx = x;const int& rx = x;
f(x); // T is int, param’s type is intf(cx); // T is int, param’s type is intf(rx); // T is int, param’s type is int
• Param will be a copy of whatever is passed in.
1. If expr’s type is a reference, ignore the reference part.
2. If expr is const, ignore that. If it’s volatile, also ignore that.
Array Argumentstemplate<typename T>void f(T param);
// The type of an array passed to a template function by value is deduced// to be a pointer type.const char name[] = “J. P. Briggs”;f(name); // T deduced as const char * // Arrays decay into pointer.
template<typename T>void f(T& param); // passed to a template function by reference
f(name); // T deduced to be const char [13] // Param’s type is const char (&)[13]
Array Arguments
template<typename T, std::size_t N>constexpr std::size_t arraySize(T (&)[N]) noexcept{ return N;}
int keyVals[] = {1, 3, 7, 9, 11, 22, 35};int mappedVals[arraySize(keyVals)];std::array<int, arraySize(keyVals)> mappedVals;
• The ability to declare references to array enables creation of a template that deduces the number of elements that an array contains.
Function Arguments
void someFunc(int, double); // type is void (int, double)
template<typename T>void f1(T param); // pass by value
template<typename T>void f2(T& param); // pass by reference
f1(someFunc); // param’s type is void (*)(int, double) // Function types decay into function pointers.
f2(someFunc); // param’s type is void (&)(int, double)
auto type deduction
auto x = 27;
const auto cx = x;
const auto& rx = x;
template<typename T>void x(T param);x(27)
template<typename T>void cx(const T param);cx(x);
template<typename T>void rx(const T& param);rx(x);
With only one exception, auto type deduction is template type deduction.
1. The type specifier is a pointer or reference, but not a universal reference. 2. The type specifier is a universal reference. 3. The type specifier is neither a pointer nor a reference.
auto type deduction
auto x1 = 27; // intauto x2(27); // intauto x3 = {27}; // std::initializer_list<int>auto x4{27}; // std::initializer_list<int>
When the initialiser for an auto-declared variable is enclosed in braces, the deduced type is a std::initializer_lists.
auto x = {11, 23, 9};template<typename T>void x(T param);
x({11, 23, 9}); // error! can’t deduce type
auto assumes that a braced initialiser represents a std::initializer_list, but template type deduction doesn’t.
template<typename T>void x(std::initializer_list<T> param);
x({11, 23, 9}); // T deduced as int, param’s type is std::initializer_list<int>
auto type deductionC++14: 1. permit auto to indicate that a function’s return type should be deduced. 2. lambdas may use auto in parameter declarations.
these use of auto employ template type deduction, not auto type deduction.
auto createInitList(){ return {1, 2, 3}; // error: can’t deduce type for {1, 2, 3} }
• Given a name or an expression, decltype tells you the name’s or the expression’s type.
• The primary use for decltype is declaring function templates where the function’s return type depends on its parameter types.
decltype
// C++11template<typename Container, typename Index>auto autoAndAccess(Container& c, Index i) -> decltype(c[i]){ authenticateUser(); return c[i]; }
// C++14template<typename Container, typename Index>auto autoAndAccess(Container& c, Index i){ authenticateUser(); return c[i]; // return type deduced from c[i] }
• To prevent to remove reference-ness of an initialising expression. (Rule 3 in template type deduction.)
decltype
// C++14template<typename Container, typename Index>decltype(auto) autoAndAccess(Container& c, Index i){ authenticateUser(); return c[i]; // return type deduced from c[i] }
std::deque<int> d;authAndAccess(d, 5); // d[5] returns an int&
decltype
// C++14Widget w;const Widget& cw = w;
auto myWidget1 = cw; // myWidget1’s type is Widget // const-ness and reference-ness will be removed
decltype(auto) myWidget2 = cw; // myWidget2’s type is const Widget&
decltype// Accept left-reference and right-reference container// C++11template<typename Container, typename Index>autoauthAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i]){ authenticateUser(); return std::forward<Container>(c)[i];}
// C++14template<typename Container, typename Index>decltype(auto)authAndAccess(Container&& c, Index i){ authenticateUser(); return std::forward<Container>(c)[i];}
decltype• Applying decltype to a name yields the declared type for that name.
• For lvalue expressions more complicated than names, decltype ensures that the type reported is always an lvalue reference.// C++14decltype(auto) f1(){ int x = 0; … return x; // decltype(x) is int, so f1 returns int}
decltype(auto) f2(){ int x = 0; … return (x); // decltype((x)) is int&, so f2 returns int& // return a reference to a local variable! (undefined behavior)}
auto - must be initialised• auto variables have their type deduced from their initialiser, so they
must be initialised.
int x1; // potentially uninitialisedauto x2; // error! initialiser requiredauto x3 = 0; // fine, x’s value is well-defined
auto - avoid verbose
template<typename It>void dwim(It b, It e){ while (b != e) { typename std::iterator_traits<It>::value_type currValue = *b; … }}
template<typename It>void dwim(It b, It e){ while (b != e) { auto currValue = *b; … }}
• avoid verbose variable declarations
auto - hold a closure• std::function is a template in the C++11 Standard Library that
generalised the idea of a function pointer. • Using std::function is not the same as using auto.
• An auto-declared variable holding a closure has the same type asthe closure, it uses only as much memory as the closure requires.
std::function<bool(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&)> derefUPLess = [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2) { return *p1 < *p2; };
// C++11auto derefUPLess = [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2) { return *p1 < *p2; };
// C++14auto derefUPLess = [](const auto& p1, const auto& p2) { return *p1 < *p2; };
auto• The type of a std::function-declared variable holding a closure is an
instantiation of the std::function template, and that has a fixed sizefor any given signature.
• The std::function approach is generally bigger and slower than theauto approach, and it may yield out-of-memory exceptions.
std::function<bool(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&)> derefUPLess = [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2) { return *p1 < *p2; };
// C++11auto derefUPLess = [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2) { return *p1 < *p2; };
// C++14auto derefUPLess = [](const auto& p1, const auto& p2) { return *p1 < *p2; };
auto• Use auto to avoid potential bugs and performance issues.std::vector<int> v;unsigned sz = v.size(); // potential bugs. unsigned is 32-bits on 64-bits machine // v.size()’s type is std::vector<int>::size_type
auto sz = v.size(); // sz’s type is std::vector<int>::size_type
std::unordered_map<std::string, int> m;// Key value is const, so actual type is std::pair<const std::string, int>// There will be a temporary variable of std::pair<std::string, int>// Every loop has an extra constructor and destructorfor (const std::pair<std::string, int>& p : m) { …}
// avoid temporary variable// simply bind the reference p to each element in mfor (const auto& p : m) { …}
auto• operator[] for std::vector<bool> doesn’t return a reference to
an element of the container. C++ forbids references to bits. • It returns an object of type std::vector<bool>::reference
std::vector<bool> features(const Widget& w);
Widget w;auto highPriority = features(w)[5]; // deduced type is std::vector<bool>::reference // highPriority is a reference to a temporary // variable. It is a dangling pointer.processWidget(w, highPriority); // undefined behaviour!
std::vector<bool> features(const Widget& w);
Widget w;bool highPriority = features(w)[5]; // std::vector<bool>::reference implicit // convert to boolprocessWidget(w, highPriority);
auto• std::vector<bool>::reference is a proxy class that exists for the
purpose of emulating and augmenting the behaviour of some other type. • As a general rule, proxy classes don’t play well with auto. • Objects of such classes are often not designed to live longer than a
single statement.
// solutionstd::vector<bool> features(const Widget& w);
Widget w;auto highPriority = static_cast<bool>(features(w)[5]); // explicitly typedprocessWidget(w, highPriority);
// avoid thisauto someVar = expression of proxy class type
Brace Initialiser• There are three methods to initialise variables.
int x(0); // initialiser is in parenthesesint y = 0; // initialiser follows “=“int z{ 0 }; // initialiser is in braces
• There were some situations where c++98 had no way to express a desired initialisation.
std::vector<int> v{1, 3, 5}; // (C++11) v’s initial content is 1, 3, 5
Brace Initialiser• Brace initialiser is universal initialiser.// C++11. Braces can be used to initialise non-static data members.class Widget { …private: int x{0}; // fine, x’s default value is 0 int y = 0; // also fine int z(0); // error!};
// uncopyable objects may be initialised using braces or parentheses.std::atomic<int> ai1{0}; // finestd::atomic<int> ai2(0); // finestd::atomic<int> ai3 = 0; // error!
Brace Initialiser• Brace initialiser prohibits implicit narrowing conversions.
double x, y, z;
int sum1{x + y + z}; // error! sum of doubles may not be expressible as intint sum2(x + y + z); // okayint sum3 = x + y + z; // okay
Brace Initialiser• std::initializer_list overshadows other constructors to the point
where the other overloads may hardly be considered.
class Widget {public: Widget(int i, bool b); Widget(int i, double d); Widget(std::initializer_list<long double> il); …};
Widget w1(10, true); // call first ctorWidget w2{10, true}; // call std::initializer_list ctorWidget w3(10, 5.0); // call second ctorWidget w4{10, 5.0}; // call std::initializer_list ctor
Brace Initialiser• Most developers end up choosing one kind of delimiter as a default, using
the other only when they have to.
nullptr• 0 and NULL are integral type, not pointer type. • nullptr’s type is std::nullptr_t.
The type implicitly converts to all raw pointer types.
void f(int);void f(bool);void f(void*);
f(0); // call f(int), not f(void*)
f(NULL); // might not compile, but typically calls f(int)
nullptr• improve code clarity
auto result = findRecord();
if (result == 0) { // the return value may be int or pointer …}
auto result = findRecord();
if (result == nullptr) { // the return value must be pointer, it is more clear. … }
nullptr• template type deduction
template<typename FuncType, typename MuxType, typename PtrType>auto lockAndCall(FuncType func, MuxType mutex, PtrType ptr) -> decltype(func(ptr)){ using MuxGuard = std::lock_guard<MuxType>;
MuxGuard g(mutex); // lock mutex return func(ptr); // call function } // unlock mutex
int f1(std::shared_ptr<Widget> spw); // call these only whendouble f2(std::unique_ptr<Widget> upw); // the appropriatebool f3(Widget* pw); // mutex is locked
std::mutex f1m, f2m, f3m;
auto result1 = lockAndCall(f1, f1m, 0); // error! ptr’s type is deduced to intauto result2 = lockAndCall(f2, f2m, NULL); // error!auto result3 = lockAndCall(f3, f3m, nullptr); // fine. ptr’s type is std::nullptr_t // implicitly convert to Widget*
Alias Declarations// use typedeftypedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS;typedef void (*FP)(int, const std::string&);
// use alias declarationusing UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>using FP = void (*)(int, const std::string&);
Alias Declarations
// alias declarationtemplate<typename T>using MyAllocList = std::list<T, MyAlloc<T>>;
MyAllocList<Widget> lw;
// typedeftemplate<typename T>struct MyAllocList { typedef std::list<T, MyAlloc<T>> type;};
MyAllocList<Widget>::type lw;
• Alias declarations may be templatized, while typedefs cannot.
Alias Declarations
template<typename T>struct MyAllocList { typedef std::list<T, MyAlloc<T>> type;};
template<typename T>class Widget {private: typename MyAllocList<T>::type list; // MyAllocList<T> may be a specialization. // “type” may be a data member of it // The names of dependent types must // be preceded by typename …};
• typedef inside a template class
Scoped enum
enum Color { black, white, red }; // unscoped enumauto white = false; // error! white already declared in this scope
enum class Color { black, white, red }; // scoped enum. avoid name pollutionauto white = false; // fine.
• The name of unscoped enum belong to the scope containing the enum.
Scoped enum
enum class Color { black, white, red }; // scoped enum
std::vector<std::size_t> primeFactors(std::size_t x);
Color c = Color::white; // fine.if (c < 14.5) { // error! can’t convert to double auto factors = primeFactors(c); // error! can’t convert to std::size_t …}
• Scoped enum is strongly typed
Scoped enum
enum class Color; // fine.enum Color; // error!
• Scoped enum could be forward-declared
• If unscoped enum wants to be forward-declared, it needs to specifyunderlying type.
enum Color: std::uint8_t; // forward-declaration for unscoped enum
deleted function
// C++98template <class charT, class traits = char_traits<charT> >class basic_ios : public ios_base {public: …
private; basic_ios(const basic_ios& ); // not defined (link-time error) basic_ios& operator=(const basic_ios& ); // not defined}
• prevent calling some particular functions
// C++11template <class charT, class traits = char_traits<charT> >class basic_ios : public ios_base {public: // C++ checks accessibility before deleted status. // Let compiler show more clear messages. … basic_ios(const basic_ios& ) = delete; // if used, it’s a compile-time basic_ios& operator=(const basic_ios& ) = delete; // error. …}
deleted function
template<typename T>void processPointer(T* ptr);
template<>void processPointer<void>(void*) = delete;
template<>void processPointer<char>(char*) = delete;
• prevent use of template instantiations that should be disabled
// template member functionclass Widget {public: template<typename T> void processPointer(T* ptr) { … }};
template<>void Widget::processPointer<void>(void*) = delete;
override
class Base {public: virtual void mf1() const; virtual void mf2(int x); virtual void mf3() &;};
class Derived : public Base {public: virtual void mf1() const override; virtual void mf2(int x) override; virtual void mf3() & override;};
• make explicit that a derived class function is supposed to overridea base class version
Prefer const_iterators to iterators
std::vector<int> valuesauto it = std::find(values.cbegin(), values.cend(), 1983);values.insert(it, 1998);
• The standard practice of using const whenever possible dictates that youshould use const_iterators any time you need an iterator.
• There is no portable conversion from a const_iterator to an iterator,not even with a static_cast.
constexpr objects
int sz;constexpr auto arraySize1 = sz; // error! sz’s value not known at compilationstd::array<int, sz> data1; // error!
constexpr auto arraySize2 = 10;std::array<int, arraySize2> data2; // fine. arraySize2 is constexpr
int sz2;const auto arraySize = sz2; // fine. arraySize is const copy of sz2std::array<int, arraySize> data; // error! arraySize’s value not known at compilation
• constexpr indicates a value that’s not only constant, it’s knownduring compilation.
• constexpr objects are, in fact, const, and they do have values that areknown at compile time.
constexpr functions
constexprint pow(int base, int exp) noexcept{ …}
constexpr auto numConds = 5;std::array<int, pow(3, numConds)> results; // call pow function at compile-time
auto base = readFromDB(“base”);auto exp = readFromDB(“exponent”);auto baseToExp = pow(base, exp); // call pow function at runtime
• constexpr functions produce compile-time constants when they are calledwith compile-time constants. If they’re called with values not known untilruntime, they produce runtime values.
constexpr
// C++11constexpr int pow(int base, int exp) noexcept{ return (exp == 0 ? 1 : base * pow(base, exp - 1));}
// C++14constexpr int pow(int base, int exp) noexcept{ auto result = 1; for (int i = 0; i < exp; i++) result *= base;
return result; }
• In C++11, constexpr functions may contain no more than a singleexecutable statement: a return.
constexpr
class Point {public: constexpr Point(double xVal = 0, double yVal = 0) noexcept : x(xVal), y(yVal) {} constexpr double xValue() const noexcept { return x; } constexpr double yValue() const noexcept { return y; } void setX(double newX) noexcept { x = newX; } // setX could be constexpr in C++14 void setY(double newY) noexcept { y = newY; }private: double x, y;};
constexpr Point p1(9.4, 27.7); // “runs” constexpr actor during compilationconstexpr Point p2(28.8, 5.3);
constexpr Point midpoint(const Point& p1, const Point& p2) noexcept{ return { (p1.xValue() + p2.xValue()) / 2, (p1.yValue() + p2.yValue()) / 2};}
constexpr auto mid = midpoint(p1, p2);
• constructors and other member functions may be constexpr
make const member functions thread safe
• roots doesn’t change Polynomial object on which it operates, but aspart of its caching activity, it may need to modify rootVals androotsAreValid.
• roots is declared const, but it’s not thread safe.
class Polynomial {public: using RootsType = std::vector<double>; RootsType roots() const { if (!rootsAreValid) { … rootsAreValid = true; } return rootVals; }
private: mutable bool rootsAreValid{ false }; mutable RootsType rootVals{}; };
make const member functions thread safe• std::mutex is a move-only type. A side effect of adding m to Polynomial
is that Polynomial loses the ability to be copied.
class Polynomial {public: using RootsType = std::vector<double>; RootsType roots() const { std::lock_guard<std::mutex> g(m); if (!rootsAreValid) { … rootsAreValid = true; } return rootVals; }
private: mutable std::mutex m; mutable bool rootsAreValid{ false }; mutable RootsType rootVals{}; };
make const member functions thread safe• std::atomic variables are often less expensive than mutex. • For a single variable or memory location requiring synchronization, use
of a std::atomic is adequate.
class Point {public: double distanceFromOrigin() const noexcept { ++callCount; return std::sqrt((x * x) + (y * y)); };
private: mutable std::atomic<unsigned> callCount{ 0 }; double x, y;};
special member function generation• the default constructor • the destructor • the copy constructor • the copy assignment operator • the move constructor (C++11) • the move assignment operator (C++11)
class Widget {public: Widget(Widget&& rhs); // move constructor widget& operator=(Widget&& rhs); // move assignment operator};
special member function generation• the move constructor (C++11) • the move assignment operator (C++11)
• perform “memberwise moves” on the non-static data members • “memberwise moves” are more like member wise move requests. • memberwise move consists of move operations on data members
and base classes that support move operations, but a copy operationfor those that don’t.
• the two move operations are not independent. • declaring a move constructor prevents a move assignment operator
from being generated. • declaring a move assignment operator prevents compilers from
generating a move constructor. • move operations won’t be generated for any class that explicitly declares
a copy operation. • C++11 does not generate move operations for a class with a user-declared
destructor. • The Rule of Three: if you declare any of a copy constructor, copy
assignment operator, or destructor, you should declare all three.
special member function generation• So move operations are generated for classes only if these three things are
true: • No copy operations are declared in the class. • No move operations are declared in the class. • No destructor is declared in the class.
std::unique_ptr• std::unique_ptr embodies exclusive ownership semantic.
• Copying a std::unique_ptr isn’t allowed. • By default, that destruction would take place via delete, but, during
construction, std::unique_ptr objects can be configured to use customdeleters.
auto delInvmt = [](Investment* pInvestment) // accept a raw pointer as argument { makeLogEntry(pInvestment); delete pInvestment; };
template<typename… Ts>std::unique_ptr<Investment, decltype(delInvmt)> // specified in unique_ptrmakeInvestment(Ts&&… params){ std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt); if ( /* a Stock object should be created */ ) { pInv.reset(new Stock(std::forward<Ts>(params)…); } return pInv;}
std::unique_ptr• Deleters that are function pointers generally cause the size of astd::unique_ptr to grow from one word to two.
• For deleters that are function objects, the change in size depends onhow much state is stored in the function object.
• Stateless function objects (lambda expressions) incur no size penalty.
std::unique_ptr• std::unique_ptr comes in two forms, one for individual objects )std::unique_ptr<T>) and one for arrays (std::unique_ptr<T[]>).
• One of its most attractive features is that it easily and efficiently converts toa std::shared_ptr.
• std::shared_ptr can’t work with arrays.
std::shared_ptr• When the last std::shared_ptr pointing to an object stops pointing there,
that std::shared_ptr destroys the object it points to. • A std::shared_ptr can tell whether it’s the last one pointing to a resource
by consulting the resource’s reference count. • std::shared_ptr are twice the size of a raw pointer. • Memory for the reference count must be dynamically allocated. • Increments and decrements of the reference count must be atomic.
std::shared_ptr Custom Deleters• For std::unique_ptr, the type of the deleter is part of the type of the smart
pointer. For std::shared_ptr, it’s not. • Specifying a custom deleter doesn’t change the size of a std::shared_ptr
object.auto loggingDel = [](Widget* pw) { makeLogEntry(pw); delete pw; };
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);std::shared_ptr<Widget> spw(new Widget, loggingDel);
pointer to T
pointer to Contrl Block
std::shared_ptr<T>
T Object
Reference Count
Weak Count
Custom Deleter
std::shared_ptr Control Block• An object’s control block is set up by the function creating the firststd::shared_ptr to the object. • std::make_shared always creates a control block. • A control block is created when a std::shared_ptr is constructed from
a unique-ownership pointer. • When a std::shared_ptr constructor is called with a raw pointer, it
creates a control block.
auto pw = new Widget; // The creation of the raw pointer is bad.std::shared_ptr<Widget> spw1(pw, loggingDel); // create control blockstd::shared_ptr<Widget> spw2(pw, loggingDel); // create 2nd control block // It will cause undefined behavior.
std::shared_ptr<Widget> spw1(new Widget, loggingDel); // direct use of newstd::shared_ptr<Widget> spw2(spw1); // spw2 uses same control block as spw1
std::shared_ptrstd::vector<std::shared_ptr<Widget>> processedWidgets;
class Widget {public: void process(); …};
void Widget::process(){ processWidgets.emplace_back(this); // pass raw pointer to shared_ptr // This is wrong! // It may cause undefined behaviour. }
std::shared_ptrstd::vector<std::shared_ptr<Widget>> processedWidgets;
// The Curiously Recurring Template Pattern (CRTP)class Widget : public std::enable_shared_from_this<Widget> {public: // To prevent clients from calling member functions that invoke shared_from_this // before a std::shared_ptr points to the object. template<typename… Ts> static std::shared_ptr<Widget> create(Ts&&… params); // factory method … void process(); …private: … // private ctors};
void Widget::process(){ processWidgets.emplace_back(shared_from_this()); // There must be an existing // std::shared_ptr that points // to the current object. }
std::weak_ptr• A pointer like std::shared_ptr that doesn’t affect an object’s
reference count. • std::weak_ptr isn’t a standalone smart pointer. It’s an augmentation ofstd::shared_ptr.
auto spw = std::make_shared<Widget>();std::weak_ptr<Widget> wpw(spw); // wpw points to same Widget as spw.spw = nullptr; // wpw now dangles.if (wpw.expired()) … // if wpw doesn’t point to an object…
// “check then create” should be atomic.// creating a shared_ptr from the std::weak_ptr// 1. use weak_ptr::lockstd::shared_ptr<Widget> spw1 = wpw.lock(); // if wpw’s expired, spw1 is nullauto spw2 = wpw.lock(); // same as above, but uses auto// 2. use constructorstd::shared_ptr<Widget> spw3(wpw); // if wpw’s expired, throw std::bad_weak_ptr
std::weak_ptr• Caching • Observer design pattern
• keep a list of observers • Backward pointer
// caching examplestd::unique_ptr<const Widget> loadWidget(WidgetID id); // costly version
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id) // caching version{ static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache; auto objPtr = cache[id].lock(); if (!objPtr) { objPtr = loadWidget(id); // load the object cache[id] = objPtr; // cache it } return objPtr; }
A B Cstd::shared_ptr std::shared_ptr
std::weak_ptr
std::make_unique && std::make_shared• std::make_shared is part of C++11. • std::make_unique is part of C++14. • This form of the function doesn’t support arrays or custom deleters. • std::allocate_shared acts like std::make_shared, except
its first argument is an allocator object to be used for the dynamicmemory allocation.
// make_unique implementation for C++11template<typename T, typename… Ts>std::unique_ptr<T> make_unique(Ts&&… params){ return std::unique_ptr<T>(new T(std::forward<Ts>(params)…)); }
auto upw1(std::make_unique<Widget>()); // with make functionstd::unique_ptr<Widget> upw2(new Widget); // without make function
auto spw1(std::make_shared<Widget>()); // with make functionstd::shared_ptr<Widget> spw2(new Widget); // without make function
std::make_unique && std::make_sharedint computePriority();
// Before calling processWidget,// * The expression “new Widget” must be evaluated// * The constructor for the std::shared_ptr<Widget>// * computePriority must runprocessWidget(std::shared_ptr<Widget>(new Widget), computePriority());
// If compiler emits code as follow// 1. Perform “new Widget”// 2. Execute computePriority// 3. Run std::shared_ptr constructor// If (2) produces an exception, the dynamically allocated memory will be leaked.
// std::make_shared has no such problems.processWidget(std::make_shared<Widget>(), computePriority());
std::make_unique && std::make_shared// Two allocations.// 1. allocate memory for Widget// 2. allocate memory for control block of std::shared_ptrstd::shared_ptr<Widget> spw(new Widget);
// One allocation suffices.// Caveat: Widget memory will not free until no one uses control block.auto spw = std::make_shared<Widget>();
std::make_unique && std::make_shared• None of the make functions permit the specification of custom deleters. • Within the make functions, the perfect forwarding code uses parentheses,
not braces. • Using make functions to create objects of types with class-specific versions
of operator new and operator delete is typically a poor idea.
std::make_unique && std::make_shared• Exception safe code without make functions.void processWidget(std::shared_ptr<Widget> spw, int priority);void cusDel(Widget *ptr); // custom deleter
// Put allocation of the Widget and the construction of the std::shared_ptr// into one statement.std::shared_ptr<Widget> spw(new Widget, cusDel); // exception safeprocessWidget(std::move(spw), computePriority()); // use move() to turn lvalue // to rvalue. It is more // efficient.
// Because processWidget’s first parameter is passed by value, construction// from an rvalue entails only a move, while construction from an lvalue requires// a copy.
Pimpl Idiom// Original classclass Widget {public: Widget(); …private: std::string name; std::vector<double> data; Gadget g1, g2, g3;};
// Pimpl Idiomclass Widget {public: Widget(); ~Widget(); // door is needed
private: struct Impl; // declare implementation struct Impl *pImpl; // and pointer to it};
Pimpl Idiom (raw pointer)#include “widget.h” // in impl. file “widget.cpp”#include “gadget.h”#include <string>#include <vector>
struct Widget::Impl { std::string name; std::vector<double> data; Gadget g1, g2, g3;};
Widget::Widget(): pImpl (new Impl) {}
Widget::~Widget(){ delete pImpl; }
Pimpl Idiom (std::unique_ptr)
class Widget { // in header “widget.h”public: Widget(); ~Widget(); // declaration only
Widget(Widget&& rhs); // declaration only Widget& operator=(Widget&& rhs);
Widget(const Widget& rhs); // declaration only Widget& operator=(const Widget& rhs);
private: struct Impl; // incomplete type declaration std::unique_ptr<Impl> pImpl;};
Pimpl Idiom (std::unique_ptr)#include “widget.h” // in impl. file “widget.cpp”#include “gadget.h”#include <string>#include <vector>
struct Widget::Impl { std::string name; std::vector<double> data; Gadget g1, g2, g3;};
Widget::Widget(): pImpl(std::make_unique<Impl>()) {}
// dtor use static_assert to ensure that the// raw pointer is complete typeWidget::~Widget() = default; // destructor needs to see complete type
// generate code to destroy pImpl when exception arises// (need complete type)Widget::Widget(Widget&& rhs) = default;
// need to destroy the object pointed to by pImpl// before reassigning it. (need complete type)Widget& Widget::operator=(Widget&& rhs) = default;
Widget::Widget(const Widget& rhs) // deep copy: pImpl(std::make_unique<Impl>(*rhs.pImpl)) {}Widget& Widget::operator=(const Widget& rhs){ *pImpl = *rhs.pImpl; return *this; }
std::move
// C++11template<typename T>typename remove_reference<T>::type&& // ensuring that && is applied to a type that isn’t a reference.move(T&& param){ using ReturnType = typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);}
// C++14template<typename T>decltype(auto) move(T&& param){ using ReturnType = remove_reference_t<T>&&; return static_cast<ReturnType>(param); }
• std::move does nothing but cast its argument to an rvalue.
std::move
class Annotation {public: explicit Annotation(const std::string text) : value(std::move(text)) // move will convert to const std::string&& { … } // It will call copy ctor, not move ctor. …private: std::string value;};
class string {public: … string(const string& rhs); // copy ctor string(string&& rhs); // move ctor};
• Don’t declare objects const if you want to be able to move from them. • std::move doesn’t guarantee that the object it’s casting will be eligible
to be moved.
std::forward
void process(const Widget& lvalArg);void process(Widget&& rvalArg);
template<typename T>void logAndProcess(T&& param) // universal reference{ auto now = std::chrono::system_clock::now(); makeLogEntry(“Calling ‘process’”, now); process(std::forward<T>(param)); // T will convey rvalue or lvalue information.}
Widget w;logAndProcess(w); // call with lvaluelogAndProcess(std::move(w)); // call with rvalue
• std::forward casts to an rvalue only if its argument was initialised withan rvalue.
rvalue reference and universal reference
void f(Widget&& param); // rvalue reference
Widget&& var1 = Widget(); // rvalue reference
auto&& var2 = var1; // universal reference (auto declaration)
template<typename T>void f(std::vector<T>&& param); // rvalue reference
template<typename T>void f(T&& param); // universal reference (function template parameter)
template<typename T>void f(const T&& param); // rvalue reference
• Universal references arise in two context. • function template parameters (It must be precisely “T&&”.) • auto declarations
• What these contexts have in common is the presence of type deduction.
rvalue reference and universal referencetemplate<class T, class Allocator = allocator<T>>class vector {public: void push_back(T&& x); // rvalue reference template <class… Args> void emplace_back(Args&&… args); // universal reference …};
auto timeFuncInvocation = [](auto&& func, auto&&… params) // universal reference { // start timer; std::forward<decltype(func)>(func)( std::forward<decltype(params)>(params)…); // stop timer and record elapsed time; };
Use std::forward on universal reference// Example 1class Widget {public: template<typename T> void setName(T&& newName) // universal reference { name = std::move(newName); } // bad idea to use move() on universal reference …private: std::string name;};
std::string getWidgetName();Widget w;auto n = getWidgetName();w.setName(n); // move n into w // n’s value now unknown (potential error!)
// Example 2template<typename T>FractionreduceAndCopy(T&& frac){ frac.reduce(); return std::forward<T>(frac); // move rvalue into return value, copy lvalue }
Use std::move on rvalue referenceMatrixoperator+(Matrix&& lhs, const Matrix& rhs){ lhs += rhs; return std::move(lhs); // move lhs into return value (more efficient)}
Matrixoperator+(Matrix&& lhs, const Matrix& rhs){ lhs += rhs; return lhs; // copy lhs into return value }
Widget makeWidget(){ Widget w; … return std::move(w); // DO NOT apply move() on local variable. // It will hinder RVO. // If the conditions for the RVO are met, but // compiler choose not to perform copy elision, // the object being returned must be treated as // an rvalue. (std::move is implicitly applied.)}
Avoid overloading on universal references
template<typename T>void logAndAdd(T&& name) // (1){ auto now = std::chrono::system_clock::now(); log(now, “logAndAdd”); names.emplace(std::forward<T>(name)); }
void logAndAdd(int idx) // (2){ auto now = std::chrono::system_clock::now(); log(now, “logAndAdd”); names.emplace(nameFromIdx(idx));}
short nameIdx;logAndAdd(nameIdx); // Error! It will be resolved to logAndAdd(short&& name). Match (1). // Exactly match (1). // Promotion to match (2). Function (1) is better than (2).
• Functions taking universal references are the greediest functions in C++.They instantiate to create matches for almost any type of argument.
Avoid overloading on universal referencesclass Person {public: template<typename T> explicit Person(T&& n) // (1) : name(std::forward<T>(n)) { … }
explicit Person(int idx) // (2) : name(nameFromIdx(idx)) { … }
Person(const Person& rhs); // (3) copy ctor (compiler-generated) Person(Person&& rhs); // (4) move ctor (compiler-generated) …
private: std::string name;};
Person p(“Nancy”);auto cloneOfP(p); // Error! It will be resolved to Person(Person&). Match (1).
const Person cp(“Nancy”);auto cloneOfP(cp); // Calls copy ctor. Match (1) and (3). // Normal function is preferred.
Avoid overloading on universal referencesclass Person {public: template<typename T> explicit Person(T&& n) // (1) : name(std::forward<T>(n)) { … }
explicit Person(int idx) // (2) : name(nameFromIdx(idx)) { … }
Person(const Person& rhs); // (3) copy ctor (compiler-generated) Person(Person&& rhs); // (4) move ctor (compiler-generated) …
private: std::string name;};
class SpecialPerson : public Person {pubic: SpecialPerson(const SpecialPerson& rhs) // copy ctor : Person(rhs) { … } // Error. Calls (1), not base class copy ctor (3) SpecialPerson(SpecialPerson&& rhs) // move ctor : Person(std::move(rhs)) { … } // Error. Calls (1), not base class move ctor (4)};
Tag Dispatchtemplate<typename T>void logAndAdd(T&& name){ logAndAddImpl( std::forward<T>(name), std::is_integral<typename std::remove_reference<T>::type>() // remove reference before // pass to is_integral<T>() );}
template<typename T>void logAndAddImpl(T&& name, std::false_type){ auto now = std::chrono::system_clock::now(); log(now, “logAndAdd”); names.emplace(std::forward<T>(name));}
void logAndAddImpl(int idx, std::true_type){ logAndAdd(nameFromIdx(idx)); }
std::enable_if
class Person {public: // syntax: enable_if<condition>::type // is_base_of<T1, T2>::value is true if T2 is derived from T1 // std::decay<T>::type removes reference and cv-qualifiers(i.e., const or volatile) from T template< typename T, typename = typename std::enable_if< !std::is_base_of<Person, typename std::decay<T>::type >::value && !std::is_integral<typename std::remove_reference<T>::type >::value >::type > explicit Person(T&& n); // universal reference constructor … };
• std::enable_if gives you a way to force compilers to behave as if aparticular template didn’t exist.
Reference collapsing
When a lvalue is passed as an argument, T is deduced to be an lvalue reference.When an rvalue is passed, T is deduced to be a non-reference.
template<typename T>void func(T&& param);
Widget widgetFactory(); // function returning rvalue
Widget w; // a variable (an lvalue)
func(w); // T deduced to be Widget&. // func(Widget& && param) => func(Widget& param)
func(widgetFactory()); // T deduced to be Widget
• Reference to reference is illegal. • Compiler may produce reference to reference in particular contexts. • If either reference is an lvalue reference, the result is an lvalue reference.
Otherwise the result is an rvalue reference. • A universal reference isn’t a new kind of reference, it’s actually an rvalue
reference in a context where two conditions are satisfied: • Type deduction distinguishes lvalues from rvalues. Lvalues of type T
are deduced to have type T&, while rvalues of type T yield T as theirdeduced type.
• Reference collapsing occurs.
Perfect forwarding failure cases
template<typename T>void fwd(T&& param){ f(std::forward<T>(param)); }
// variadic formtemplate<typename… Ts>void fwd(Ts&&… params){ f(std::forward<Ts>(params)…);}
f(expression); // if this does one thing,fwd(expression); // but this does something else, fwd fails // to perfectly forward expression to f
• We want the forwarded-to function to be able to work with theoriginally-passed-in objects.
• Perfect forwarding fails when either of the following occurs: • Compilers are unable to deduce a type for one or more of
fwd’s parameters • Compilers deduce the “wrong” type for one or more of fwd’s
parameters
Perfect forwarding failure cases braced initializers
void f(const std::vector<int>& v); // declaration
f({1, 2, 3}); // fine, “{1, 2, 3}” implicitly converted to std::vector<int>
fwd({1, 2, 3}); // error! Compilers are forbidden from deducing a type for the // expression {1, 2, 3} in the call to fwd, because fwd’s parameter // isn’t declared to be a std::initializer_list.
// workaroundauto il = {1, 2, 3}; // il’s type deduced to be std::initializer_list<int>fwd(il);
Perfect forwarding failure cases Declaration-only integral static const data member
class Widget {public: static const std::size_t MinVals = 28; // Declaration-only, no memory allocated for it. …};
std::vector<int> widgetData;widgetData.reserve(Widget::MinVals); // Compilers will do const propagation.size_t* pt = &Widget::MinVals; // Link errors. Pointers need memory to address.
f(Widget::MinVals); // Fine, treated as f(28)fwd(Widget::MinVals); // Link errors. Reference is like pointer. It needs a location to address.
// workaroundconst std::size_t Widget::MinVals; // Give a definition in Widget’s .cpp file.
Perfect forwarding failure cases Overloaded function names and template names
int processVal(int value);int processVal(int value, int priority);
f(processVal); // Finefwd(processVal); // Error! There is no type information on processVal. No type deduction.
template<typename T>T workOnVal(T param){ …}
fwd(workOnVal); // Error! Which workOnVal instantiation? workOnVal represents many functions.
// workaroundusing ProcessFuncType = int (*)(int);ProcessFuncType processValPtr = processVal; // Use a variable to select which function you want.fwd(processValPtr); // Fine. processValPtr has type information. It could do type deduction.
fwd(static_cast<ProcessFuncType>(workOnVal)); // Fine. Select workOnVal<int> as parameter.
Perfect forwarding failure cases Bitfields
struct IPv4Header { std::uint32_t version:4, IHL:4, DSCP:6, ECN:2, totalLength:16, …};
void f(std::size_t sz);
IPv4Header h;
f(h.totalLength); // Fine.fwd(h.totalLength); // Error. There’s no way to bind a reference to arbitrary bits.
// workaroundauto length = static_cast<std::uint16_t>(h.totalLength); // Make a copy.fwd(length); // pass the copy to fwd