5 min. read

Smart pointers

Avoid:

1
2
3
4
5
6
7
8
9
10
11
class MyService{};

class DependentOnMyService
{
~DependentOnMyService()
{
delete mService;
}

MyService* mService;
}

Better:

1
2
3
4
5
6
7
class MyService{};

class DependentOnMyService
{
private:
std::unique_ptr<MyService> mService;
}

Reasoning:

  • RAII is used to control the lifetime of allocated objects.
  • Pointers and references are needed to support polymorphism, handle with care!
  • Avoid using new and delete manually.
  • When you need a single memory ownership use std::unique_ptr.
  • When you need a multiple memory ownership use std::shared_ptr.

Generics and TypeDefs (Using)

Avoid:

1
2
3
4
5
6
7
8
9
void Send(Message message, InnerType type)
{
auto* inner = message.GetInner();

if(inner->Type == InnerType::String)
{
mStream.Send(message);
}
}

Better:

1
2
3
4
5
6
7
8
9
10
11
12
template <class TMsg, class TType = typename TMsg::inner_type>
void Send(TMsg message)
{
mStream.Send<TMsg, TType>(message);
}

// Example for a message class:
class Message
{
public:
using inner_type = std::string;
};

Reasoning:
Generics help us to write reuseable code.
We accomplish this by passing the parameters as generic types.

In this example we make use of type definitions using inner_type = std::string.
Instead of providing an enum we use the inner_type definition from within the type.

Rule of 0, 3, 5

Avoid:

1
2
3
4
5
class A
{
A() { std::cout << "A\n"; }
A(const A&) { std::cout << "A&\n"; }
};

Better:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A
{

};

class B
{
B(const B&) = default;
~B() = default;
B& operator=(const B&);
};

class B
{
A(const A&) { std::cout << "A&\n"; }
A(A&&) { std::cout << "A&&\n"; }
~A() { std::cout << "~An"; }
A& operator=(const A&) = default;
A& operator=(A&&) = default;
};

Reasoning:
Keep track of your explicit resources.
Using these rules you can avoid memory leaks and bad assignments.

Rule of 0 - If a class doesn’t define custom ctors, copy or dtors, it’s not necessary to add them to the class.

Rule of 3 - If a class defines dtor, copy ctor or copy assignment, it most needs all 3 of them.

Rule of 5 - If a class requires a move ctor, move assignment it most need all 5 of them.

Feature checks

Avoid:

1
2
3
4
5
void MyFunc(const char* someBuffer)
{
std::span<char>{};
std::cout << "From Span " << someBuffer;
}

Better:

1
2
3
4
5
6
7
8
9
void MyFunc(const char* someBuffer)
{
#ifdef __cpp_lib_mdspan
std::span<char> s{ someBuffer };
std::cout << "From Span " << someBuffer;
#else
std::cout << "From Char* " << someBuffer;
#endif
}

Reasoning:
Generic libraries may need backward compatability for older compiler toolsets.
Any code that needs to be agnostic to it may use the Feature checks.
This can assist to transfer new features to older code bases.

Another way to handle it is to specify a minimum C++ version for your library.
Or add which C++ versions it supports.

Concepts - C++ 20

Avoid:

1
2
3
4
5
6
7
8
9
10
11
class Parser
{
public:
virtual void* Parse() = 0;
};

void Func(Parser* parser)
{
auto* data = parser.Parse();
// Do something with data
}

Better:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <typename T>
concept HasParse =
requires(T t) {
{ t.Parse() };
};

template <HasParse T>
auto Func(T obj)
{
return obj.Parse();
}

class A
{
public:
int Parse()
{
return 2;
}
};

Reasoning:
To constraint the parameters, we can use Runtime checks and polymorphism.
Yet it also bind us to specific types.
Concepts constraint our parameters through the means of generic types.
This would benefit us when we are passing the “wrong” object:

Let’s assume we made a typo in the function:

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
template <typename T>
concept HasParse =
requires(T t) {
{ t.Parse() };
};


template <class T>
auto FuncNoConstraint(T obj)
{
return obj.Parse();
}

template <HasParse T>
auto FuncWihtConstraint(T obj)
{
return obj.Parse();
}

class A
{
public:
int Pares()
{
return 2;
}
};

A a;
auto resA = FuncNoConstraint<A>(a);
auto resB = FuncWithConstraint<A>(a);

When using the function FuncNoConstraint we get the error in the function itself:

1
Error	C2039	'Parse': is not a member of 'A'	TestingCPPApp		

When using the function FuncWithConstraint we get a specific error in the right location:

1
2
Error	C2672	'FuncWithConstraint': no matching overloaded function found	TestingCPPApp		


C++ is a powerful language with so much to learn :)
Thanks for reading!


Is this helpful? You can show your appreciation here: Buy me a coffee