打开导航 打开导航 Open menu
量化交易系统开发(C++)/ 期权策略与风控 / 行业研究分析
量化交易系统開發(C++)/ 期权策略与风控 / 行业研究分析
C++ trading systems / options strategy & risk controls / equity research
adrian@adrianxv.cn
文章目录

some best practices in modern C++ some best practices in modern C++ some best practices in modern C++

Question 42 ~ 50 from the book Daily C++ Interview

Use Brace Initialization

Brace initialization refers to all initialization using {}, inculding:

  1. list initialization
  2. aggregate initialization

List initialization

List initialization is a broader term than initialization that uses a std::initializer_list<T> constructor. And std::initializer_list<T> is nothing more than a lightweight proxy object providing access to an array of const T objects.

You can either use or not use a = in list initialization:

  1. direct-list initialization
int a{1};
std::string s{"hello"};
std::vector<int> v{1, 2, 3, 4, 5};
  1. copy-list initialization
int a = {1};
std::string s = {"hello"};
std::vector<int> v = {1, 2, 3, 4, 5};

The difference is that direct-list initialization allows calling explicit constructors, whereas copy-list initialization rejects it:

struct A {
    explicit A(int x) {}
};

A a{100};     // OK
A b = {100};  // Error

Aggregate initialization

Aggregate initialization is also called brace initialization. It was added in C++ 11. It is specifically for aggregate types, such as structs.

struct myStruct {
    int a;
    int b;
};

myStruct s{1, 2};

the beneifit of using brace initialization

  1. Not prone to the most vexing parse

“the most vexing parse” refers to the ambiguity when something that looks like an object declaration is interpreted as a function declaration.

This code declares a function that takes no arguments and returns a std::string

std::string s();

But by changing the () into {}, you get an object declaration.

  1. It doesn’t allow narrowing conversions
int i{3.14};           // Error: narrowing from double to int
unsigned int j{-5};    // Error: narrowing from int to unsigned int

explicit constructors

The explicit specifier specifies that a constructor cannot be used for implicit conversions. Implicit conversion happens when an object is expected (as an argument or right operand), but only the arguments of the constructor are given. Thus the corresponding construcor is invoked first to construct an object, which is then passed in.

class myClass {
    myClass(int x) : i_(x) {}
    int i_;
};

void foo(myClass obj) {
    // ...
}

foo(42);          // implicit conversion
myClass obj = 50; // implicit conversion

Such conversions can happen multiple times with built-in types. For example, if we passed in std::size_t{42} or char{'a'} into foo(), the implicit conversion would still work (which is infamous for hiding bugs). But with user defined types, implicit conversion happens only once, with an example below:

struct A {
    A(int x) {}
};
struct B {
    B(A a) {}
};
B = 42; // Error: requires two user-defined conversions.

If we specified myClass::myClass(int x) as explicit, then foo(42) and myClass obj = 50 would fail to compile. Which saves us from unexpected behavior (that hides bugs). In fact, it is suggested to make every single-argument constructor explicit.

User-defined literals

So what even are literals? Literals are values written into source code. This example shows some built-in literals:

int x = 42;         // 42 is a literal of type int
std::string s = ""; // "" is an empty string literal

C++11 introduced user-defined literals, which are flexible in usages, but are mostly used for numerical conversions and strong typing. Here’s an example of it used in numerical conversions:

constexpr double operator""_km (int meters) {
    return double{meters} / 1000;
}

std::cout << "50 meters is " << 50_km << "kilometers";

Use nullptr over NULL or 0

NULL is a legacy macro representing a null pointer. After C++11, nullptr is preferred over NULL.

The type of nullptr itself is std::nullptr_t, which is not itself a pointer type, but it implicitly converts to all raw poninter types. That’s why we can use it wherever a specific pointer is expected.

There are further advantages of using nullptr related to template metaprogramming. // TODO: learn it.

Use alias over typedef

typedef is used to avoid typing complex type names. The difference between typedef and auto for that purpose is that auto deduces the type from the constructor, whereas typedef explicitly states the type, so typedef doesn’t require definition on the spot.

typedef std::array<OMOrder, SideToIndex(Side::MAX) + 1> OMOrderHashMap;

Since C++11, alias declaration does exactly what typedef does plus support for using templates. Thus alias is preferred over typedef.

using OMOrderHashMap = std::array<OMOrder, SideToIndex(Side::MAX) + 1>;

template<typename T>
using Vec = std::vector<T>;

Another advantage of alias over typedef is that it is more readable with function pointers:

typedef void (*MyFunctionPointer)(int, int);
using MyFunctionPointerAlias = void (*)(int, int);

Use scoped enums over unscoped enums

Scoped and unscoped enums serve the same purpose of representing a value chosen from a set of named constants. Unscoped enums are introduced in C++98:

enum Transmission {
    manual,
    automatic
};

Transmission mode = manual;

Whereas scoped enums use the class keyword. They are essentially classes. Enumerators are called with the class name, which brings better clearity.

enum class Transmission {
    manual,
    automatic
};

Transmission mode = Transmission::manual;

Both scoped and unscoped enums support specifying the underlying type, which is defaulted to int.

enum Transmission : uint32_t {...};
enum class Transmission : uint32_t {...};

= delete

Before =delete was introduced in C++11, the way to treat non-copyable objects is to declare their copy constructor and assignment operator as private, and not defining them (not providing implementation). This way, calling the function outside the class would cause a “accessing private methods” error; and calling the function within the class would cause a linker error (because their is no implementation).

But the =delete approach after C++11 is better because it is more explicit, and copy semantics is disabled at compile time.

Deleted functions should be declared as public, otherwise some compilers might complain about calling a private function, not that it’s deleted.

= delete disallows implicit conversion for function params

Say, there is a function (either member or free) that takes in an integer, and we want to avoid the caller from passing in a double. If we do nothing, the passed in double would just be converted into int. In order to raise an error when the caller does pass in a double, we can = delete that overloaded function.

void foo(int x) {
    // ...
}

void foo(double) = delete;

foo(3.5); // ERROR: use of deleted function void foo(double)

Trivial Class/Struct in C++

Let’s review that there are 6 special member functions in a class:

  1. The default constructor
  2. Destructor (only one. No need to specify “default”)
  3. The copy constructor
  4. The move constructor
  5. The copy assignment operator
  6. The move assignment operator

And that “explicitly defaulted special member functions” refers to = default:

In this case, we use = default to explicitly tell the compiler to generate a default constructor for us. Otherwise, the compiler would not implicitly generate a default constructor because we have defined MyClass(int x).

class MyClass {
    MyClass(int x);
    MyClass() = default;
};

A trivial class/struct is a class/struct that has only compiler-provided or explicitly defaulted special member funtions. Trivial classes:

  • cannot have virtual member functions.
  • cannot inherit from a base class with non-trivial constructor/operator/destructor
  • cannot have data members of non-trivial types

We can check if a class is trivial:

#include <type_traits>
std::cout << std::is_trivial(A)::value;

The memory layout of a trivial class object is fairly simple: a contiguous region containing only bits that represent member variables plus padding, because methods are compiled as code and not stored within the object’s memory. The memory layout of a trivial class object might look like this. Note that the specific sequence depends on the compiler.

struct Trivial {
    char c;
    int i;
};

std::size_t x = sizeof(Trivial); // 8, not 5.

// potential memory layout:
// char:    1 byte
// padding: 3 bytes
// int:     4 bytes

By using #pragma pack(push, 1), we tell the compiler to use 1-byte alignment, which means that an object can start at multiples of 1, effectively any memory address, rather than multiples of 2, 4, 8, etc, and thus eliminate padding.

#pragma pack(push, 1)
struct Trivial {
    char c;
    int i;
};
#pragma pack(pop)

std::size_t x = sizeof(Trivial); // 5

// potential memory layout:
// char:   1 byte
// int:    4 bytes

A trivially copyable type can be copied into a byte buffer and copied back to restore the original value. You can check wheteher an object is trivially copyable. // TODO: (C++20) learn more about what makes an object trivially copyable, and what prevents it.

#include <type_traits>
static_assert(std::is_trivially_copyable_v<A>);

Next, we’re going to talk about why trivial classes cannot have virtual functions. Virtual functions and inheritance are separate topics. Virtual function supports runtime polymorphism. A class can be a base class without having any virtual functions. That way, the function called by a pointer depends on the pointer’s static type, not the object’s runtime type. So the below behavior produces “Base” rather than “Derived”.

class Base {
    void foo() { std::cout << "Base" << std::endl; }
};

class Derived : Base {
    void foo() { std::cout << "Derived" << std::endl; }
};

Base* p = new Derived{};
p->foo();  // Base

Whereas if we made Base::foo virtual, p->foo() would produce “Derived”.

Runtime metadata (Vptr and vtable) is what supports runtime polymorphism. The vptr is a hidden pointer stored inside the base class object that points to a vtable (virtual function table). The vtable contains virtual function addresses, and is shared by objects of the same class. On a 64-bit system, that means every base class object contains 8 bytes of vptr data. So the memory layout for a base class object might look like this:

vptr:    8 bytes
char:    1 byte
padding: 3 bytes
int:     4 bytes
padding: 4 bytes

You can verify whether a class is trivial or not with std::is_trivial