Language Runtime Enhancements

Reference: https://github.com/changkun/modern-cpp-tutorial

Lambda expression

Basic Syntax:
[capture list] (parameter list) mutable(optional) exception attribute -> return type {
// Function body
}

1
2
3
4
// Example
update(m, [](std::string key) -> long long int {
return;
});
  • capture list: a type parameter -> can serve to transfer external data to lambda expression.
    • Value Capture: Similar to parameter passing by value - Generated when it is created not when called.
      • value = 1, lambda (value), value = 100, lambda call -> lambda value = 1
    • Reference Capture: parameter passing by refernce
      • lambda value = 100
    • Implicit capture: Writing capture list is tired, so compiler supports many implicit ways
      • [] - empty capture list
      • [name1, name2] - captures series of variables
      • [&] - reference capture: compiler will decide which one to refer itself.
      • [=] - value capture: compiler execute the list of derivation applications itself.
    • Expression Capture: Above value and reference caputre capture lvalue (variable name) as it captures the ones that are outer scope. This capture allows captured members to be initialized with arbitrary expressions; rvalues. Type is being judged like auto.
1
2
3
4
5
6
7
8
9
10
// expression capture
void lambda_expression_capture(){
auto important = std::make_unique<int>(1);
// std::move : accept rvalue reference parameters - take the rvalue and move it
auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int{
// Now value of important is not specified
return x+y+v1+(*v2); //as v2 = unique ptr
};
std::cout << add(3,4) << std::endl;
}

Generic Lambda

  • We learned auto cannot be used in the parameter list as it would conflict with the functionality of the template.
  • Lambda expression is not templated -> the parameter table cannot be generalized, hence the parameter table type must be clarified -> From C++14 you can use auto.
1
2
3
4
5
6
7
8
9
// From C++14, you can use auto keyword to generate generic meanings.
// Below is C++11

void lambda_generic(){
auto generic = [](auto x, auto y){
return x+y;
};
//...
}

Function Object Wrapper

std::function - generic, polymorphic function wrapper whose instances can store, copy, and call any target entity that can be called; a container of functions. -> We can more easily handle functions and function pointers as objects.

std::bind - solves the requirement that we may not always be able to get all the parameters of a function at one time.

std::placeholder::_N - Indicate nth element is not given yet, so when the parameters are given later, it fills from the front.

rvalue Reference

It solves a large number of historical issues in C++; extra overheads for vector,string, and allows the function object container, std::function possible.

  • lvalue: is a persistent object that still exists after an expression (not necessarily assignment expression). 주소값을 받을 수 있는 모든 것.
  • rvalue: is the temporary object that no longer exists after the expression ends. 참조 할 수 없는 값.
    • move(a) 는 a를 rvalue로 바꾸어서 lvalue 로 이동하는것; unconditionally conver lvalue -> rvalue
    • pvalue (pure): purely rvalue/literal like 10, true. Temporary variables returned by non-references, temp variables generated by operation expressions, origin literals, lambda expressions.
    • xvalue (expiring): A value that is destroyed but can be moved. -> T&&
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void reference(std::string& str){
std::cout << "lvalue" << std::endl;
}

void reference(std::string&& str){
std::cout << "rvalue" << std::endl;
}

int main(){
std::string lv1 = "string"; // lv1 is lvalue, "string" is rvalues
// std::string&& r1 = lv1; // illegal as rvalue cannot ref to lvalue.
std::string&& rv1 = std::move(lv1); // Legal -> move(lv1) convers lv1 to rvalue.

const std::string& lv2 = lv1 + lv1;

std::string&& rv2 = lv1 + lv2; // legal as lv1 + lv2 is a temp rvalue object.
// That is why below is Possible
rv2 += "new string";

reference(rv2); // output: lvalue -> because rv2 refers to rvalue -> rv2 is lvalue

int &a = std::move(1); // illegal - non-const lvalue ref cannot ref rvalue
const int &b = std::move(1); // legal

// lvalue must be const reference to reference rvalue
}

Move Semantics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

class A{
public:
int *pointer;
A() : pointer(new int(1)) {
cout << "ctor" << pointer << endl;
}

A(A& a) : pointer(new int(*a.pointer)){
cout << "copy" << pointer << endl;
} // meaningless object copy

// Move implementation
A(A&& a) : pointer(a.pointer){
a.pointer = nullptr;
cout << "move" << pointer << endl;
}

~A(){
cout << "dtor" << pointer << endl;
delete pointer;
}
};

A return_rvalue(bool test){
A a, b; // two ctor called
if (test) return a; // equal to static_cast<A&&>(a);
else return b; // equal to static_cast<A&&>(b);
}

int main(){
// rhs is xvalue: moving A(A(A&&)) to obj which is a pointer as rvalue
// and the pointer = nullptr -> object b becomes obj
// Therefore there is no dtor called
A obj = return_rvalue(false); // return b
}
// outputs:
// ctor 0xe7be70 (a)
// ctor 0xe7c2a0 (b)
// move 0xe7c2a0 (obj)
// dtor 0 (b)
// dtor 0xe7be70 (a)
// dtor 0xe7c2a0 (obj)

Above do not use copy constructs -> enhance performance

Perfect Forwarding
No matter what type of reference type the template parameter is, the template parameter can bederived as right reference type if and only if the argument type is a right reference.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void reference(std::string& str){
std::cout << "lvalue" << std::endl;
}

void reference(std::string&& str){
std::cout << "rvalue" << std::endl;
}

template <typenameT>
void pass(T&& v){
reference(v);
reference(std::move(v));
reference(std::forward<T>(v));
}

int main(){
pass("a"); // rvalue references
string b = "a";
pass(b); // lvalue references

// if both int -> lvalue???
// Depending on compilers?
}

reference contraction rule

Language Usability Enhancements

Reference: https://github.com/changkun/modern-cpp-tutorial

  • Language behaviour that occurred before the runtime.

Constants

nullptr:

  • CPP does not allow implicit conversion of void * to other types.
  • Traditional CPP treats null and 0 same.
  • So to certain how to deal with NULL in cpp, use nullptr
  • By doing so we can do the right function calls.

constexpr:

  • Expression produces the same result without any side effects.
  • Compiler will optimise and embed it at compile time.
  • It is useful as CPP array length must be constant expression; yet most compilers these days still support as part of its optimisation.
  • Therefore, use constexpr for array length so that it can be compiled.
1
2
3
4
// Below is C++11 form, from 14, you can write like a function.
constexpr int fib(const in n){
return n == 1 || n == 2 ? 1 : fib(n-1) + fib(n-2);
}

Variables and Initialisation

IF Statement
In C++17, following is available like Go

1
2
3
4
5
6
7
8
// Original
auto itr = std::find(vec.begin(), vec.end(), 2);
if (itr != vec.end()) *itr = 3;

// CPP17 - can define a local variable, and check the condition
if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()){
*itr = 4;
}

Initialiser_List

  • It is the one that allows to initialise the different type with same syntax: {} introduced in C++11
  • Auto used in followings:
    • Used with {} at Initialisation and assignment expression.
    • Binded to auto including ranged for.
  • It can be implemented with 2 ptr, or 1 ptr and size.
  • Be aware that it is a shallow copy which does not guarantee the data for the original instance deleted.
  • Possible errors in narrowing conversion like int to double.
  • Which means we can use itr for array.

Structured Binding

  • Provide functionality similar to the multiple return values
  • Use std::tuple to return multiple values in tuple form.
  • But, unfortunately, we have to clearly define how many values the returned tuple has.
1
2
3
4
5
6
std::tuple<int, double, std::string> f(){
return std::make_tuple(1, 2.3, "45");
}
...
// auto will be applied indiv to each variables.
auto [x, y, z] = f();

Type Inference

auto

  • Cannot be used for function arguments.
  • Cannot be used to derive array types: auto arr[10] = arrOrigin; // cannot infer array type.

decltype

  • Used to solve the defect that the auto keyword can only type that variable -> use expression!
    1
    2
    3
    4
    auto x = 1; // int
    auto y = 2.1; // double
    decltype(x+y) z; // double
    z = 1; // z is still double = 1.0.

tail type inference

Quick Note: In specifying a template (naming), class and typename are interchangeable = equivalent. However: special cases are

  • In the case of dependent types. typename is used to declare when referencing a nested type that depends on another template parameter such as typedef as following.

  • When you declare a template template parameter: must use class.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Hint at the compiler that you are referring to a dependent type
    template<typename param_t>
    class Foo{
    typedef typename param_t::baz sub_t;
    };

    // declaring a template template
    template <template class T> class C {}; // valid!
    template <template typename T> class C{}; // Invalid!

    tail type inference: use decltype in specifying template.

  • Since the compiler will not know which type is the input parameter, decltype must be defined as a tail to define template’s return type as following:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // C++11
    template<typename T, typename U>
    auto add(T x, U y) -> decltype(x+y){
    return x+y;
    }

    // C++14
    template<typename T, typename U>
    auto add(T x, U y){
    return x+y;
    }

    // use link
    auto w = add<int, double>(1, 2.0); // return is 3.0 as double.

decltype(auto)

  • It deals with the concept of parameter forwarding in C++, will cover later.
  • Mainly to derive the return type of a forwarding function or package, which does not require us to explicitly specify the parameter expression of decltype.
  • 쉽게 말하면, 그냥 리턴타입 귀찮은거 알아서 해준다는 소리.

Control Flow

if constexpr in C++17

  • 간단하게 if statement 에서 constexpr 사용할 수 있게되었다.
1
2
3
4
5
template<typename T>
auto func(const T& t){
if constexpr (std::otherFunc<T>::value) do something;
else do somethingElse;
}

Templates

Philosophy: throw all problems at compile time so that only deals with core dynamic services at runtime; optimise the performance of the run time.

Templates

  • When compiler sees fully defined template, it will instantiate it in each compilation unit.
  • 만약 정의만 하고 사용하지 않는다? 컴파일 되지도 않음 (디버깅 하면 함수가 존재도 안함) -> 이 이유로 다른 함수처럼 헤더에 선언만 하면 안됨. 어디든 정의도 같이.
  • 만약 더하기 함수를 정의하고 int 와 double 두 가지 자료형으로 사용한다면 -> 컴파일 후 해당 템플릿의 int, double 두가지 형식의 함수가 생김.

Extern Templates

  • By using extern, we can explicitly tell the compiler when to instantiate the template.
  • 만약 다른 컴파일 유닛에 해당 템플릿과 동일한게 존재 한다면 (같은 자료형을 받는), 한곳에 extern 을 사용하여 해당 템플릿이 단 한번만 컴파일 되게 지정한다 (성능 향상)
1
2
template class std::vector<bool>; // force instantiation
extern template class std::vector<double>; // Should not instantiate in current file.

Type alias templates & Default template Parameter

  • Template is not type so cannot use typedef, but we want to make a certain type for a template like followings
  • You can set default type for template parameter like a function
1
2
3
4
5
6
7
8
9
10
// Template Type Alias
template <typename T>
using soModern = someType<std::vector<t>, std::string>;
int main(){
soModern<bool> isIt;
}

// Default template parameters
template <typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y){return x+y;}

Variadic Templates - 가변 길이 템플릿

  • 말 그대로 가변으로써 여러 패러미터들을 따로 명시하지 않고 … 처럼 벌크로 넘길 수 있는 방식.
  • bulk 로 주어진 args 를 unpack 하는 방법에는 보통 다음과 같은 형태를 따른다.
  1. Recursive Template Function
  • Most classic approach that we can think of.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename TO>
void printf1(TO value){
cout << value << endl;
}

template<typename T, typename... Ts>
void printf1(T value, Ts... args){
cout << value << endl;
printf1(args...); // when # args = 1 -> trig above func
}

int main(){
printf1(1,2,"asdf", 1.1);
return 0;
}
  1. Variable parameter template expansion
    Very cumbersome - can use std::bind

    1
    2
    3
    4
    5
    tempalte<typename TO, typename... T>
    void printf2(TO t0, T... t){
    std::cout << t0 << std::endl;
    if constexpr (sizeof...(t) > 0) printf2(t...);
    }
  2. Initialise list expansion
    Recursive Functions are a standard practice -> drawback is that you must define a terminate function.

1
2
3
4
5
6
7
8
template<typename T, typename... Ts>
auto printf3(T value, Ts... args){
std::cout << value << std::endl;
// Lambda Expression
(void) std::initializer_list<T>{([&args])}{
std::cout << args << std::endl;
}(), value)...};
}

Fold expression

1
2
3
4
// Available from C++17
template <typename ... T>
auto sum(T ... t) return (t + ...);
int main(){std::cout << sum(1,2,3,4,5) << std::endl;}
  • Inheritance constructor: using Parent::Parent; // Inherit parent constructor.

Explicit Virtual function overwrite

1
2
3
4
5
6
7
8
9
struct Base {
virtual void foo(); //abstract function - will be overriden by child object.
};

struct Child : Base {
// Can be two options: Override parent func | Add the same name func.
void foo();
// If base foo() gets deleted -> Child has its own method -> undefined behaviour.
};

To Avoid above situation C++11 introduced override and final.

Override

  • explicitly tells the compiler to overload -> compiler check the base func if it has a virtual function, otherwise throw error: virtual void foo() override;

Final

  • To prevent the class inherit and to terminate the virtual function to be overloaded.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct Base{
    // No one can inherit & overload this function (so no same name).
    virtual void foo() final;
    };

    // No one can inherit this Child class.
    struct Child final : Base {
    // Legal, but Child cannot be parent because of final.
    };

Delete and default

As discussed, compiler generates default constructor, destructor, and copy assignment if not defined.

But if you want to prohibit certain behaviour, it must be defined in private -> not comfty.

By using default and delete, you can remain it in the public scope like following:

1
2
3
4
5
class Base {
public:
Base() = default; // Telling compiler to use default generated ctor.
Base& operator=(const Base&) = delete; // to refuse constructor copy assignment (but remain it in the public)
}

Terminology

Reference: Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs

Terminology

Declaration: Tells compilers about the name and type of something, but not details.

1
2
3
4
5
6
7
8
extern int x; // object declaration

// std defines c++ standard library
// size_t both exists in C and C++
// signature of a function: return type and its parameter types
std::size_t numDigits(int number) // func declaration
class Widget;
template<typename T> class GraphNode(T smallT) // template declaration

Definition: provides compilers with the details a declaration omits. It is where compilers set aside memory for the object.

1
2
3
4
5
int x; // object definition
std::size_t numDigits(int number){
// function code
}
...

Initialisation:

  • process of giving an object its first value.
  • For objects of user-defined types -> initialisation performed by constructors.
  • Default Constructor: can be called without any arguments or has default values.

Explicit:

  • it prevents from being used to perform implicit type conversions, but may be used for explicit type conversions.
  • Explicit constructors are more preferable than non-explicits as it prevents compilers from performing unexpected type converstions.
  • If you dont have good reason to do implicit type conversion -> declare it as explicit!
1
2
3
4
5
6
class B{
explicit B(int x= 0, bool b = true);
};
void doSomething(B object); // function takes B as an input

doSomething(B(28)); // explicit type convert for int 28 to object B

Copy Constructor: copy the another object of the same type.
Copy Assignment Operator: to copy the value from one object to another of the same type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A{
public:
A(); // default constructor
A(const A& rhs); // copy constructor
A& operator=(const A& rhs); // copy assignment operator
};

A a1; // Invoke def constr
A a2(a1); // Invoke copy constr
a1 = a2; // Invoke copy assignment operator
A a3 = a2; // Invoke copy constructor

void tempFunc(A a); //
tempFunc(a3); // a3 passed by value -> copy constructor called a = a3.

How to distinguish? If a new object is being defined like a2 and a3, it is copy constructor. If an existing object copies another like a1 = a2, it is copy assignment constructor.

Possible undefined behavrious:

1
2
3
4
5
int *p = 0; // p is a nullptr
std::cout << *p; // dereferencing a nullptr -> undefined

char name[] = "sixcha"; // array of size 7 including trailing null
char c = name[10]; // Referencing invalid array index.

Accustoming Yourself to C++

Reference: Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs

Prefer consts, enums, and inlines to #defines

Prefer the compiler to the preprocessor because #define may be treated as if it is not part of the language per se.

1
2
3
4
5
6
#define TEMP 1.65 // It may never be seen by compilers -> may not in symbol table -> causes problem.
// So replace as below is better
const double Temp = 1.65;
// uppercase usually for macros
// above def seen by compilers and be in symbol tables
// const float may yield smaller code than #define

When replacing #define with constants be aware:

  1. Defining constant pointers: Important that the ptr to be constant

    • const char * const varName = “value”;
    • varName is a const ptr that points const char which is “value”
    • For string: const std::string varName(“value”);
  2. Class-specific constants: ensure only one copy of it -> static like following

    • Usually CPP requires definition except class-specific constants of integeral type (int, chars, bools…).
      1
      2
      3
      4
      5
      6
      7
      8
      class Temp{
      private:
      static const int var = value;
      // constant declaration + definition
      // It may be illegal for older compilers, as was illegal to provide initial value for static member. If that,
      static const int old;
      };
      const int Temp::old = 1; // def goes in impl. file
  3. If, somehow, statis must have value during compilation, use enum hack

    • Somehow behaves more #define than a const
    • It’s legal to take address of const, but enum is not.
    • Typically, not legal to take address of #define
    • So, if you don’t want user get a ptr or reference of your constants, use enum
    • Enum hack is a fundamental technique of template metaprogramming that you can know when you see it what is it for.
      1
      2
      3
      4
      class Temp{
      private:
      enum {VarName = 5}; // VarName a symbolic name for 5
      };
      Don’t use #define macro like MAX_IS(a,b) f((a…)), but use inline type template instead.
      1
      2
      3
      4
      5
      6
      7
      #define CALL_WITH_MAX ... 
      // Instead use
      template <typename T>
      inline void callWithMax(const T& a, const T& b){
      f(a > b ? a : b);
      }
      // Can have efficiency of macro (less overhead for calling a function) + predictable behaviour + type safety

Use const whenever possible

It allows you to specify a semantic constraint -> particular object not be modified

1
2
3
4
5
6
7
char greeting[] = "HI";
char *p = greeting; // non const ptr, non const data
const char *p = greeting; // non const ptr, const data
char * const p = greeting; // const ptr, non const data
const char * const p = greeting; // const ptr, data.
// Remember to read it from the right side.
// last one: p is a const ptr that points const char

Declaring an iterator const = declaring a const ptr
Check the following:

1
2
3
4
5
6
7
8
9
10
11
12
// itr = T * const
const std::vector<int>::iterator itr = vec.begin();
// Valid, and invalid
*itr = 10; ++itr;

// itr = const T*
std::vector<int>::const_iterator cItr = vec.begin();
*cIter = 10; // Invalid!
++cIter; // valid

// const iterator: a const iterator for a value
// const_iterator: an iterator for a const value
1
2
3
4
const Temp operator*(const Temp& lhs, const Temp& rhs);
// const here will return const Temp object which prevents the returned object to be modified
Temp operator*() const;
// const here will ensure no modification on this object as passed by const Temp& this implicitly. but can modify mutable member

To remember:

  • Declaring const -> helps compilers to detect usage errors.
  • Compilers enforce bitwise constness, but you should program using conceptual constness.
  • if const and non-const have same implementation, have non-cost and call const version.

Make sure to initalise objects before use

Very complicated to understand which one initialised or not -> so always initialise the objects. Dont get confused with assignment and initialisation.

1
2
3
4
5
6
7
8
Temp::Temp(a, b, c){
// all assignments
a_ = a;
b_ = b;
c_ = c;
}
Temp::Temp(a,b,c) : a_(a), b_(b) ...{} // initialisation
// Copy constructor is more efficient than default constructor + copy assignment operator call.

Order:

  • Base class is initialised before derived class.
  • Data member is initialised in the order of its declaration.

The order of initialisation of non-local static objects defined in different translation units.

  1. A static object:
    1. one that exists from the time it’s constructed until the end of the program (on heap).
      1. so stack and heap-based objects are thus excluded (as it can be deallocated before the end of the program)
      2. So include global objects, objects defined at namespace scope, objects declared static inside classes, and inside functions and objects declared static at file scope.
    2. Static objects inside functions: local static objects
    3. Others are: non-local static objects.
  2. A translation unit: source code giving rise to a single object file - a basic unit of cpp compilation (source file + files included as #include).

It becomes an issue when it involves at least two separately compiled source files: Each contains non-local static object with dependency because the relative order of initialization of non-local static objects defined in different translation units is undefined

Problem solved entirely by set both non-local static object to be local static objects -> singleton pattern.

  • CPP gurantee that local static objects are initialised when object’s definition is encountered when to call it.
  • By changing direct access to non-local static object to calls to functions (that returns local static..), problem gets solved.
1
2
3
4
5
6
7
8
9
// Example.
Temp& tmp(){
static Temp tp;
return tp; // return reference to this local static object
}
TempTwo::TempTwo(...){
std::size_t asdf = tmp().functionName();
// tmp() here is static object that is guaranteed to be initialised.
}

This reference-returning function is simple: define and initialise a local static object, and return.

  • It makes excellent candidates for inlining if called frequently.
  • It makes problematic in multithreaded systems; but it is just what non-const static object always problematic in any multithreaded systems.
    • To solve this, have to manually invoke the functions in single-thread to avoid initialisation-related race condition.

Summary

To avoid initialisation problems -> remember three:

  1. Manually initialise non-member objects of built-in types.
  2. Use member initialisation lists to initialise all parts of an object (In a constructor).
  3. Design to avoid that non-local static object defined in separate translation units problem.
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×