31 nooby C++ habits you need to ditch

mCoding
13 Dec 202116:18

Summary

TLDRThis video highlights 31 common mistakes made by beginners in C++. It covers bad practices such as using 'namespace std' globally, misusing 'std::endl' in loops, and relying on C-style arrays instead of standard arrays. The video emphasizes the importance of using standard algorithms, proper casting, const correctness, and structured bindings. It also warns against unnecessary heap allocations, ignoring compiler warnings, and misunderstanding smart pointers. The aim is to help new programmers write more efficient, readable, and maintainable C++ code.

Takeaways

  • 📌 Avoid using 'using namespace std;' globally, especially in header files, as it can lead to namespace pollution and force this choice on users of your code.
  • 🔃 Prefer using a simple newline character over 'std::endl' in loops to avoid unnecessary buffer flushing, which can degrade performance.
  • 🔄 Opt for range-based for loops when the index is not needed, reducing the chance of off-by-one errors and improving readability.
  • 🔍 Utilize standard algorithms like 'std::find_if' instead of writing custom loops for simple tasks, leveraging the power of the C++ Standard Library.
  • 📚 Use standard arrays instead of C-style arrays to benefit from automatic management of array length and to avoid pointer decay.
  • ⚠️ Be cautious with 'reinterpret_cast' as it can lead to undefined behavior; use 'bit_cast' in C++20 for safer type reinterpretation.
  • 🚫 Refrain from casting away 'const' unnecessarily, as it can lead to errors and misunderstandings about object mutability.
  • 🔑 Understand that 'operator[]' in maps can insert elements if they do not exist, which might be an unintended side effect when just trying to access an element.
  • 🔒 Prioritize 'const correctness' to ensure functions do not modify objects they should not, and communicate this intent clearly to the users of your code.
  • 🎯 Know about string literal lifetimes; they are valid for the duration of the program, allowing for safe references to them even when returning from functions.
  • 🔗 Embrace structured bindings for cleaner and more readable code when dealing with pairs or tuples, making it easier to work with complex data structures.

Q & A

  • Why is 'using namespace std' considered a bad habit, especially at the global level?

    -'using namespace std' is considered a bad habit because it can lead to name collisions and make it unclear where certain functions or objects are coming from. Using it at the global level can force this choice on everyone who uses your code, especially if done in a header file.

  • What is the main issue with using 'std::endl' in a loop?

    -Using 'std::endl' in a loop is inefficient because it not only adds a new line but also flushes the output buffer, which can significantly slow down the program. It's better to use a newline character '\n' instead.

  • Why is it preferable to use range-based for loops over index-based for loops in C++?

    -Range-based for loops are preferable because they reduce the risk of off-by-one errors and accidental typos related to index manipulation. They also make the code more expressive by focusing on the elements rather than the indices.

  • What advantage does using standard algorithms like 'std::find_if' have over writing custom loops?

    -Standard algorithms like 'std::find_if' are generally more optimized, tested, and expressive. They make the code more readable and reduce the chance of bugs since you're reusing well-known, tested code.

  • Why should one avoid using C-style arrays in C++?

    -C-style arrays should be avoided because they decay into pointers and require explicit management of the array length, increasing the risk of errors. Using standard arrays or vectors provides safer and more convenient alternatives.

  • What is the problem with using 'reinterpret_cast' in C++?

    -'reinterpret_cast' can lead to undefined behavior unless cast back to the original type. Most uses of 'reinterpret_cast' are dangerous, and safer alternatives often exist. For instance, 'bit_cast' in C++20 offers a safer way to reinterpret the bytes of an object.

  • Why is it important to understand the difference between default and value initialization?

    -Understanding the difference is crucial because default initialization leaves variables with indeterminate values, while value initialization guarantees they are set to zero or their default values. This knowledge helps prevent bugs related to uninitialized variables.

  • How can ignoring const correctness lead to issues in C++ programs?

    -Ignoring const correctness can lead to unintended modifications of data, making the code less predictable and harder to debug. Marking parameters and methods as 'const' where appropriate ensures that they do not modify the data, leading to safer and more reliable code.

  • What is the danger of adding or removing elements from a container while iterating over it?

    -Adding or removing elements from a container while iterating over it can invalidate iterators and cause undefined behavior. This often happens because operations like 'push_back' can reallocate the container, making previously valid iterators invalid.

  • Why is it recommended to use smart pointers like 'unique_ptr' and 'shared_ptr' over raw pointers?

    -Smart pointers like 'unique_ptr' and 'shared_ptr' manage the memory automatically, ensuring that resources are properly cleaned up when they are no longer needed. This helps prevent memory leaks and other resource management issues.

Outlines

00:00

📘 Common C++ Mistakes for Beginners

This paragraph outlines common habits and pitfalls that new C++ programmers should be aware of. It starts with the overuse of 'using namespace std' which can lead to namespace pollution, especially when used in header files. It then discusses the misuse of 'std::endl' for simple new lines, advocating for the newline character instead to avoid unnecessary buffer flushing. The paragraph also covers the preference for range-based for loops over indexed loops when the index is irrelevant, the use of standard algorithms instead of custom loops for common tasks, the advantages of standard arrays over C-style arrays, and the dangers of 'reinterpret_cast' and C-style casting due to undefined behavior. It concludes with advice on const correctness, string literal lifetimes, and the benefits of structured bindings for readability, as well as the recommendation to return structs instead of multiple out parameters for clarity.

05:00

🛠️ C++ Best Practices and Compile-Time Computations

This section emphasizes the importance of leveraging C++ features to write efficient and clean code. It advises against performing work at runtime that could be done at compile time, such as calculating the sum of the first n integers when n is known at compile time. It also stresses the importance of marking destructors as virtual to ensure proper cleanup in inheritance hierarchies and the correct initialization order in constructors. The paragraph further explains the difference between default and value initialization, the readability benefits of avoiding magic numbers, and the pitfalls of modifying containers while iterating over them. It also cautions against unnecessary moves of local variables and clarifies misconceptions about move semantics, evaluation order in expressions, and the guaranteed left-to-right evaluation in C++17.

10:02

🧩 Advanced C++ Memory Management Techniques

This paragraph delves into advanced memory management strategies in C++, highlighting the use of smart pointers like 'unique_ptr' and 'shared_ptr' over manual memory management with 'new' and 'delete'. It discusses the RAII pattern, which ensures resource management is tied to object lifetimes. The paragraph also points out the proper use of 'make_unique' and 'make_shared' for direct construction of smart pointers, and the misconception that raw pointers are inherently bad. It clarifies the difference between a const pointer and a pointer to const, and the thread-safety of 'shared_ptr'. Additionally, it addresses the incorrect use of shared pointers for non-shared resources and the common mistake of ignoring compiler warnings, which can lead to undefined behavior.

15:04

🔧 Final Thoughts on C++ Best Practices

The final paragraph wraps up the video script with a reminder of the importance of following C++ best practices to avoid common beginner mistakes. It reiterates the significance of understanding const correctness, the proper use of smart pointers, and the thread-safety of reference counting in 'shared_ptr'. The paragraph also emphasizes the need to heed compiler warnings and concludes with a thank you to supporters, signaling the end of the video content.

Mindmap

Keywords

💡using namespace std

In C++, 'using namespace std' is a directive that allows programmers to avoid prefixing standard library components with 'std::'. Although it saves typing, using it globally, especially in header files, can lead to namespace pollution and conflicts. The video emphasizes avoiding its use in headers to prevent forcing this choice on others who use the code.

💡std::endl

The 'std::endl' is a manipulator in C++ used to insert a newline character into an output stream and flush the stream buffer. While often used to add newlines, the video highlights that it also performs a buffer flush, which can be inefficient in loops. Instead, using '\n' is recommended for just adding newlines without flushing.

💡range-based for loop

A range-based for loop in C++ simplifies iterating over elements in a container without explicitly managing indices. The video advocates for its use over traditional for loops with indices, as it reduces the risk of errors like off-by-one mistakes and makes the code clearer and more intent-revealing.

💡reinterpret_cast

'reinterpret_cast' is a C++ operator used for type casting that allows converting any pointer type to any other pointer type. However, it's generally unsafe and can lead to undefined behavior if misused. The video advises caution with 'reinterpret_cast', recommending its use only when absolutely necessary, and preferring safer alternatives like 'bit_cast' introduced in C++20.

💡const correctness

Const correctness in C++ means using the 'const' keyword to ensure that variables or function parameters are not modified after their initial assignment. The video emphasizes marking variables and function parameters as 'const' whenever possible to communicate intent and prevent accidental modifications, enhancing code safety and clarity.

💡structured bindings

Structured bindings, introduced in C++17, allow for unpacking values from objects like pairs or tuples into separate variables. The video illustrates their use with examples, showing how they make code more readable by giving meaningful names to elements instead of using 'first' and 'second'.

💡RAII (Resource Acquisition Is Initialization)

RAII is a C++ programming idiom that ties the lifecycle of resources (like memory or file handles) to the lifespan of objects. When an object is created, it acquires resources, and when it's destroyed, it releases them. The video explains that using RAII ensures that resources are automatically managed and cleaned up, avoiding manual resource management and potential leaks.

💡smart pointers

Smart pointers in C++ are objects that manage the lifetime of dynamically allocated memory, ensuring proper cleanup. The video contrasts 'unique_ptr' and 'shared_ptr', explaining that 'unique_ptr' provides exclusive ownership while 'shared_ptr' allows shared ownership with reference counting. They are recommended over raw pointers to avoid manual memory management and leaks.

💡default vs. value initialization

In C++, default initialization leaves variables uninitialized with potentially garbage values, while value initialization sets variables to zero or a default state. The video discusses the importance of understanding the difference and advocates for using value initialization to ensure predictable and safe behavior in code.

💡evaluation order

Evaluation order in C++ refers to the sequence in which parts of an expression are computed. Prior to C++17, the order of evaluating sub-expressions was not guaranteed, which could lead to unexpected results. The video provides an example and explains that while C++17 and later standards have improved guarantees, function argument evaluation order is still unspecified and can affect program behavior when side effects are involved.

Highlights

Avoid using 'using namespace std' globally, especially in header files, to prevent namespace pollution.

Prefer the newline character over 'std::endl' in loops to avoid unnecessary buffer flushing.

Use range-based for loops instead of index-based for loops when the index is not needed, reducing the chance of errors.

Leverage standard algorithms instead of writing custom loops for common tasks, such as finding elements.

Choose standard arrays over C-style arrays to avoid issues with pointer decay and manual length passing.

Avoid 'reinterpret_cast' and undefined behavior; use 'bit_cast' in C++20 for safer type reinterpretation.

Do not cast away 'const' unnecessarily; use 'at' method for safe access in maps.

Be aware that 'operator[]' in maps can insert elements if they do not exist, which might be unintended.

Mark functions and parameters as 'const' correctly to ensure they do not modify the underlying data.

Understand string literal lifetimes; they are valid for the entire program life, allowing for safe references.

Utilize structured bindings for improved readability when dealing with pairs or tuples.

Return structs instead of multiple out parameters to give meaningful names to returned values.

Perform calculations at compile-time when possible to optimize performance.

Always mark destructors as 'virtual' to ensure proper cleanup of derived classes.

Initialize class members in the order they are declared to avoid using uninitialized values.

Differentiate between default and value initialization for predictable variable states.

Minimize the use of magic numbers by defining named constants for better readability and maintenance.

Avoid adding or removing elements from a container while iterating over it to prevent iterator invalidation.

Do not return moved local variables unnecessarily as the compiler can optimize this without explicit moves.

Understand that 'std::move' essentially casts to an rvalue reference, not necessarily moving data.

Be cautious of evaluation order in expressions; C++17 guarantees left-to-right evaluation for certain cases.

Consider stack allocation over heap allocation when possible to avoid unnecessary memory management.

Prefer 'unique_ptr' and 'shared_ptr' for automatic resource management over manual 'new' and 'delete'.

Use 'make_unique' and 'make_shared' for safer and more efficient memory allocation.

Avoid direct use of 'new' and 'delete' in favor of smart pointers that automate resource management.

Embrace RAII for automatic resource management and avoid manual resource handling.

Recognize that raw pointers are not inherently bad and can be used appropriately when ownership is clear.

Distinguish between returning a 'shared_ptr' and a 'unique_ptr' based on the expected ownership semantics.

Understand that while reference counting in 'shared_ptr' is thread-safe, the actual resource access requires synchronization.

Differentiate between 'const pointer' and 'pointer to const' to properly apply constness to pointers and data.

Pay attention to compiler warnings as they can indicate potential issues or undefined behavior.

Transcripts

play00:00

Hello and welcome to my list of newbie C++ habits.

play00:03

C++ is an incredibly complex language with a lot of history.

play00:06

So, whether you're an actual noob that really needs to look out for these things

play00:10

or whether you just want to improve your code a little bit, let's get started.

play00:13

Newbie thing number 1: using namespace std.

play00:17

People generally use it to save typing.

play00:19

So, you can say things like string instead of std::string.

play00:23

If it's limited to just a single function that might not be that bad.

play00:26

But, let's be honest it's usually used at the global level.

play00:30

Even worse, if you do this in a header file, then you also force this choice upon every one that uses your code.

play00:36

So, please don't do this in a header file...

play00:38

and also consider just using the names that you actually use.

play00:42

Number 2: using std::endl, especially in a loop.

play00:46

You probably meant to just print out a new line but endline does more than that.

play00:50

It also flushes the buffer which takes extra time.

play00:53

Instead, just use a newline character.

play00:55

Number three: using a for loop by index when a range-based for loop expresses the intent better.

play01:00

In this case, I don't really care at all about the index.

play01:03

Instead, use a range-based for loop.

play01:06

There's no index. So, one less chance for an accidental typo or off by one error.

play01:11

Number 4: using a loop when a standard algorithm already exists to do what you're trying to do.

play01:16

There's some simple thing that you want to do like find the index of the first positive number in a vector.

play01:21

It's so simple though you decide to just write it yourself.

play01:25

Instead, consider if there's an algorithm that already does what you're trying to do.

play01:30

In this case, we can use std::find_if to find where the first positive element is.

play01:34

Number 5: using a C style array when you could have used a standard array.

play01:38

C style arrays often decay into pointers and require you to pass the length of the array along with the array itself.

play01:44

This is just another opportunity to make a typo.

play01:47

Instead, use a standard array.

play01:50

Number 6: any use of reinterpret_cast.

play01:53

Pretty much the only thing you're allowed to do with an object that you got from reinterpret cast

play01:58

is to reinterpret cast it back to the original type.

play02:01

Almost everything else is undefined behavior. And yes, the same goes for C-style casting.

play02:07

So, sorry famous Quake three algorithm for computing the inverse square root.

play02:11

reinterpreting the bytes of a float as a long is actually undefined behavior.

play02:16

You're always allowed to reinterpret cast the address of an object as a character type.

play02:21

This allows you to inspect or print out the bytes that make up the object, so you can see its memory layout.

play02:26

However, as of C++ 20, this use of reinterpret cast is also not necessary.

play02:31

bit_cast interprets the bytes of one object as a different type.

play02:35

In this case, I can cast any object to an array of bytes of the same size.

play02:39

Number 7: casting away const.

play02:42

This function takes in a map that maps strings to the number of times that they've occurred.

play02:47

It takes two words and then returns back to you whichever one has a higher count.

play02:52

The first way you might try to implement this is by looking up the counts of the two words

play02:56

and then if the first count is bigger than the second then returning the first word.

play02:59

If I try to compile this, I get a weird error message telling me that the method isn't marked const.

play03:04

And that's how we ended up with this code.

play03:06

I know that I'm not modifying the map. Right?

play03:08

The correct thing to do in this case is not to cast away const.

play03:12

But to instead use the at method.

play03:14

at is a const version of operator square bracket that throws if the word isn't in the map.

play03:19

But you might ask: Why don't they just add a const version so that this would compile?

play03:24

This brings us to newbie thing number 8:

play03:26

not knowing that operator square brackets inserts the element into the map if it doesn't already exist.

play03:32

That's right, simply trying to look up this word in the map actually inserts it with a count of zero into the map.

play03:39

Number 9: ignoring const correctness.

play03:42

This function loops over a vector simply printing out each element one element per line.

play03:47

It doesn't modify the vector.

play03:49

So, we could and should mark the vector const.

play03:51

This is doubly important for a function parameter

play03:54

so that the caller knows that they can use this function without their vector being modified.

play03:59

Number 10: not knowing about string literal lifetimes.

play04:03

String literals like this one are guaranteed to live for the entire life of a program.

play04:07

So, it's perfectly fine to return this, even though it looks like it's a reference to a local variable.

play04:12

Number 11: not using structured bindings.

play04:15

Here we have a map of color names to their hex values.

play04:18

Then we just loop over all the pairs and print out the name and hex value.

play04:21

It would be a lot more readable if we could refer to these things as name and hex rather than pair.first and pair.second.

play04:28

Well, that's exactly what a structured binding lets us do.

play04:31

We grab the pair by reference.

play04:33

Then, the structured binding introduces name and hex as names for the first and second elements of the pair.

play04:39

Structured bindings can also be used with your own types if the members are public.

play04:42

The names get assigned to the variables according to the order of their declarations.

play04:47

Number 12: using multiple out parameters when you want to return multiple things from a function.

play04:52

Instead, create a basic struct and give those things names.

play04:55

Then, you can return the multiple values that you wanted to by returning the struct instead.

play05:00

The caller can even make it look like it was multiple values returned by using structured bindings.

play05:04

Number 13: doing work at runtime that could have been done at compile time.

play05:08

Here's a function that gives the formula for the sum of the first n integers.

play05:12

Sometimes, the parameters might be known at compile-time and you could do the calculation ahead of time.

play05:17

Go ahead and let the compiler know that it's totally fine to compute this ahead of time.

play05:21

Number 14: forgetting to mark destructors virtual.

play05:24

If a derived class gets deleted through a pointer to this base class,

play05:28

then the derived class destructor will not be called only the base classes destructor will be called.

play05:33

Here, I have a class that derives from the base that doesn't have a virtual destructor.

play05:37

This function expects a pointer to the base class.

play05:40

It uses some base functionality and then deletes the pointer when it's done.

play05:44

This will happen automatically since I'm using a unique pointer.

play05:47

But the same would be true if you just took in a normal pointer and then manually called delete.

play05:52

If you pass a pointer to an instance of this derived type, then the wrong destructor gets called at the end.

play05:59

To make sure the correct destructor is called even through a pointer to a base class, you need to mark the function virtual.

play06:05

It's also good practice to explicitly mark the derived classes destructor as override.

play06:10

Number 15: thinking that class members are initialized in the order they appear in the initializer list.

play06:15

Reading left to right this looks fine.

play06:17

The actual order that members are initialized in is the order that they're declared in.

play06:21

First, we initialize end as start plus size but this m start is garbage. It hasn't been initialized yet.

play06:27

We can of course fix this by declaring start first instead.

play06:31

Or, since start is also a parameter to the function, we could just define the end variable in terms of the start parameter.

play06:37

Number 16: not realizing there's a difference between default and value initialization.

play06:42

x and x2 are default initialized.

play06:44

They contain garbage until you put something into them.

play06:47

y, y2 and y3 are value initialized.

play06:50

They are guaranteed to contain the value zero.

play06:53

z, on the other hand, is neither default nor value initialized.

play06:56

This is a function declaration.

play06:58

Even if it's a tiny bit less efficient, initializing your values is almost always a good idea.

play07:03

The same kind of thing goes if you have an aggregate or array type.

play07:07

In the first two cases, n and m will be garbage and S will be the empty string.

play07:12

In the last three cases, n and m are zero and S is the empty string.

play07:16

Notice that, even with default initialization, the empty string did still get initialized.

play07:22

That's because for both default and value initialization if you define a default constructor, it will be called.

play07:28

Number 17: overuse of magic numbers.

play07:32

Introducing a basic constant in your code can make it many times more readable.

play07:37

The compiler is going to optimize it away anyway. Just give it a good name.

play07:41

Number 18: attempting to add or remove elements from a container while looping over it.

play07:46

Well, doing that is sometimes just necessary but what I mean is noobs often do it incorrectly.

play07:52

We're trying to put a copy of the vector at the end of the vector.

play07:55

Adding or removing an element to the vector may invalidate the iterators to the vector.

play08:00

For example, push_back might need to resize the vector and move all the elements to a new location.

play08:06

After moving the contents of the vector to a new location, you can't expect the end pointer to be the same.

play08:12

You would run into the exact same issue.

play08:14

In fact, it's probably clearer why you would run into this issue if you used iterators directly.

play08:20

This is a case where using a loop index actually does solve the problem.

play08:24

It doesn't matter if the contents of your vector get moved somewhere else the ith element is still the ith element.

play08:30

Number 19: returning a moved local variable.

play08:34

I totally get where you're coming from.

play08:36

A vector can be a large object and you don't want to make a copy of it.

play08:40

So, you go ahead and try to move it out.

play08:42

If you had just tried to return v directly, there would have been no copy and no move.

play08:47

In this situation, that's because of return value optimization.

play08:51

But what if the compiler can't do return value optimization.

play08:55

In all cases, the move is unnecessary.

play08:57

The compiler always knows that it can move a local variable.

play09:01

But in some cases, this actively prevents return value optimization.

play09:05

So, that's why this is one of the few rules where I can say you should just never do this.

play09:10

Which brings us to number 20: thinking that move actually moves something.

play09:15

Here is an implementation of standard move.

play09:18

The full templated definition might be a bit much to take in all at once.

play09:21

So, let's take a look just at the int case.

play09:24

Move takes in an int lvalue reference,

play09:27

static casts it to an rvalue reference and returns it.

play09:31

The exact same thing happens in the or value overload.

play09:34

It just static casts to an rvalue and returns it.

play09:37

A more accurate name for move is probably something like cast to rvalue.

play09:42

Number 21: thinking that evaluation order is guaranteed to be left to right.

play09:47

Here's a famous example.

play09:49

We have a string that says, "but i have heard it works even if you don't believe in it".

play09:53

We replace the first four characters with the empty string.

play09:56

Then, we find even and replace it with only.

play09:59

Then, we find don't and delete it.

play10:02

With this reasoning, we should end up with: "i have heard it works only if you believe in it".

play10:06

But prior to C++ 17, the compiler is actually allowed to compute any sub-expression in any order.

play10:12

So theoretically, it could find the location of even first

play10:16

and then replace the first four characters making that location off by four.

play10:21

So, then when the second replace happens it would replace these four characters with only

play10:26

and you can see how this goes on you don't get the result you expected.

play10:30

Well, the good news is that as of C++ 17, this example is guaranteed.

play10:35

If you have a.b, then a is guaranteed to be evaluated before b is.

play10:40

However, even in C++ 20, the order that function arguments are evaluated is still not guaranteed left to right.

play10:47

This wouldn't matter much if a b and c were pure functions.

play10:51

But, if a b and c have side effects, then the order that they're called in might actually make a difference.

play10:56

Number 22: using totally unnecessary heap allocations when a stack allocation would have been fine

play11:01

Here, we create two customer records on the heap.

play11:05

Then, we do some work and then we end up deleting those variables at the end of the function.

play11:09

The question we should ask ourselves is: Did this really need to be a heap allocation?

play11:14

There's a good chance it would have been fine if we just stack-allocated them.

play11:18

So, let's just say for the sake of argument that these objects are too big and you really do want them on the heap.

play11:23

That brings us to number 23: not using unique pointer and shared pointer to do your heap allocations.

play11:29

What happens if an exception gets thrown in the middle here?

play11:32

Then, these deletes never occur and the memory is leaked.

play11:35

When you want to make sure that a resource is cleaned up, you need to put that cleanup code in a destructor.

play11:40

So, why don't we make a class that holds a pointer, and then in its destructor it deletes that pointer.

play11:46

Well, that's exactly what unique_ptr does.

play11:48

You can give it a heap-allocated pointer and when it goes out of scope, it deletes it.

play11:53

A shared pointer on the other hand uses a reference-counting scheme, similar to what you might have in a language like Python.

play11:59

When the reference count hits zero as the last shared pointer goes out of scope that shared pointer is in charge of the deletion.

play12:06

This scheme is much more expensive because reference incrementing and decrementing have to be done atomically.

play12:11

That brings us to number 24: constructing a unique or shared pointer directly instead of using make unique or make shared.

play12:18

make_unique and make_shared will pass your arguments directly to the constructor of your object.

play12:23

Number 25: any use of new or delete.

play12:27

There's no reason to rewrite functionality that already exists.

play12:31

Here, I'm trying to manage the memory of some resource and then delete it when it's done.

play12:36

That's already what a unique pointer does.

play12:39

Don't try to couple the purpose of your class to the idea of ownership of an object.

play12:44

That's a completely separate issue.

play12:46

Unique pointer and shared pointer together cover pretty much every valid use of new or delete.

play12:52

Number 26: any kind of attempt to do manual resource management.

play12:57

This is pretty much the same story as with new and delete.

play13:01

If you find yourself manually freeing closing or releasing any kind of resource,

play13:06

then look to see if there's a class that does that automatically.

play13:10

By the way, this idea of having resources automatically close in a destructor is called RAII.

play13:16

It stands for Resource Acquisition Is Initialization.

play13:20

But it really has more to do with ensuring that resources are released upon destruction.

play13:25

Number 27: thinking that raw pointers are somehow bad.

play13:29

Here's a basic max function.

play13:31

This function is just reading from the pointers.

play13:33

It doesn't care at all who's in charge of deleting them.

play13:36

If your function doesn't have anything to do with the ownership of the objects in question,

play13:41

then there's no need to use smart pointers.

play13:44

The convention is that raw pointers don't own what they're pointing to.

play13:48

And this should not be confused with the const-ness of the pointer.

play13:52

This add function adds the source into the destination.

play13:55

But this function is not in charge of the lifetime of either of the pointers.

play14:00

Of course, if you're interoperating with C code, C code is not going to share this convention.

play14:05

A C-function might very well return some heap-allocated memory to you that it expects you to delete.

play14:11

Be careful though. If you've got a pointer with malloc, you need to delete it with free.

play14:16

You can't just let unique pointer try to delete it with the built-in delete.

play14:19

You can still use a unique pointer though.

play14:21

You just need to define your own deleter which uses free.

play14:25

Number 28: returning a shared pointer when you aren't sure the object is going to be shared.

play14:31

If a caller got a unique pointer, it's cheap and easy to convert it into a shared pointer if that's what they really need.

play14:37

They could even directly assign the unique pointer return value to a shared pointer.

play14:42

But if you return to them a shared pointer in the first place,

play14:45

the damage would already be done if all they really wanted was a unique pointer.

play14:48

Number 29: thinking that shared pointer is thread-safe.

play14:52

The reference counting part of the shared pointer is thread-safe.

play14:56

Here, I make a shared resource and pass it off to two worker threads.

play15:00

Because the reference counting is thread-safe,

play15:03

there's no danger that the object will fail to be deleted or be deleted twice.

play15:08

However, it's only the reference counting part that's atomic.

play15:12

This part here where we're accessing the x variable and the resource is not atomic and there's no locks.

play15:17

This is a plain old data race of two threads trying to modify the same memory.

play15:22

If you want to fix the data race, you need to fix it the way you'd fix any other data race.

play15:26

And 30: confusing a const pointer with a pointer to const.

play15:31

The concept of a const pointer versus pointer to const is pretty simple.

play15:35

But, a lot of newbies struggle to remember how to tell the difference between them syntactically.

play15:40

The rule is that const applies to whatever is immediately to its left.

play15:45

Unless it's the leftmost thing, in which it applies to the thing to its right.

play15:49

So here, the cost applies to the int, not to the pointer.

play15:52

Here, the const applies to the int, not to the pointer.

play15:56

And here, const applies to the pointer, not to the int.

play16:00

Number 31: ignoring compiler warnings.

play16:03

Ignoring them or turning them off very frequently leads to undefined behavior.

play16:08

I hope you enjoyed my list of newbie C++ habits.

play16:12

As always thank you to my patrons and donors for supporting me.

play16:16

See you next time.

Rate This

5.0 / 5 (0 votes)

Related Tags
C++ Best PracticesNewbie HabitsCode ImprovementAvoid MistakesSmart PointersConst CorrectnessCompiler WarningsMemory ManagementResource AcquisitionRAII