Hello and welcome to my list of newbie C++ habits.
C++ is an incredibly complex language with a lot of history.
So, whether you're an actual noob that really needs to look out for these things
or whether you just want to improve your code a little bit, let's get started.
Newbie thing number 1: using namespace std.
People generally use it to save typing.
So, you can say things like string instead of std::string.
If it's limited to just a single function that might not be that bad.
But, let's be honest it's usually used at the global level.
Even worse, if you do this in a header file, then you also force this choice upon every one that uses your code.
So, please don't do this in a header file...
and also consider just using the names that you actually use.
Number 2: using std::endl, especially in a loop.
You probably meant to just print out a new line but endline does more than that.
It also flushes the buffer which takes extra time.
Instead, just use a newline character.
Number three: using a for loop by index when a range-based for loop expresses the intent better.
In this case, I don't really care at all about the index.
Instead, use a range-based for loop.
There's no index. So, one less chance for an accidental typo or off by one error.
Number 4: using a loop when a standard algorithm already exists to do what you're trying to do.
There's some simple thing that you want to do like find the index of the first positive number in a vector.
It's so simple though you decide to just write it yourself.
Instead, consider if there's an algorithm that already does what you're trying to do.
In this case, we can use std::find_if to find where the first positive element is.
Number 5: using a C style array when you could have used a standard array.
C style arrays often decay into pointers and require you to pass the length of the array along with the array itself.
This is just another opportunity to make a typo.
Instead, use a standard array.
Number 6: any use of reinterpret_cast.
Pretty much the only thing you're allowed to do with an object that you got from reinterpret cast
is to reinterpret cast it back to the original type.
Almost everything else is undefined behavior. And yes, the same goes for C-style casting.
So, sorry famous Quake three algorithm for computing the inverse square root.
reinterpreting the bytes of a float as a long is actually undefined behavior.
You're always allowed to reinterpret cast the address of an object as a character type.
This allows you to inspect or print out the bytes that make up the object, so you can see its memory layout.
However, as of C++ 20, this use of reinterpret cast is also not necessary.
bit_cast interprets the bytes of one object as a different type.
In this case, I can cast any object to an array of bytes of the same size.
Number 7: casting away const.
This function takes in a map that maps strings to the number of times that they've occurred.
It takes two words and then returns back to you whichever one has a higher count.
The first way you might try to implement this is by looking up the counts of the two words
and then if the first count is bigger than the second then returning the first word.
If I try to compile this, I get a weird error message telling me that the method isn't marked const.
And that's how we ended up with this code.
I know that I'm not modifying the map. Right?
The correct thing to do in this case is not to cast away const.
But to instead use the at method.
at is a const version of operator square bracket that throws if the word isn't in the map.
But you might ask: Why don't they just add a const version so that this would compile?
This brings us to newbie thing number 8:
not knowing that operator square brackets inserts the element into the map if it doesn't already exist.
That's right, simply trying to look up this word in the map actually inserts it with a count of zero into the map.
Number 9: ignoring const correctness.
This function loops over a vector simply printing out each element one element per line.
It doesn't modify the vector.
So, we could and should mark the vector const.
This is doubly important for a function parameter
so that the caller knows that they can use this function without their vector being modified.
Number 10: not knowing about string literal lifetimes.
String literals like this one are guaranteed to live for the entire life of a program.
So, it's perfectly fine to return this, even though it looks like it's a reference to a local variable.
Number 11: not using structured bindings.
Here we have a map of color names to their hex values.
Then we just loop over all the pairs and print out the name and hex value.
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.
Well, that's exactly what a structured binding lets us do.
We grab the pair by reference.
Then, the structured binding introduces name and hex as names for the first and second elements of the pair.
Structured bindings can also be used with your own types if the members are public.
The names get assigned to the variables according to the order of their declarations.
Number 12: using multiple out parameters when you want to return multiple things from a function.
Instead, create a basic struct and give those things names.
Then, you can return the multiple values that you wanted to by returning the struct instead.
The caller can even make it look like it was multiple values returned by using structured bindings.
Number 13: doing work at runtime that could have been done at compile time.
Here's a function that gives the formula for the sum of the first n integers.
Sometimes, the parameters might be known at compile-time and you could do the calculation ahead of time.
Go ahead and let the compiler know that it's totally fine to compute this ahead of time.
Number 14: forgetting to mark destructors virtual.
If a derived class gets deleted through a pointer to this base class,
then the derived class destructor will not be called only the base classes destructor will be called.
Here, I have a class that derives from the base that doesn't have a virtual destructor.
This function expects a pointer to the base class.
It uses some base functionality and then deletes the pointer when it's done.
This will happen automatically since I'm using a unique pointer.
But the same would be true if you just took in a normal pointer and then manually called delete.
If you pass a pointer to an instance of this derived type, then the wrong destructor gets called at the end.
To make sure the correct destructor is called even through a pointer to a base class, you need to mark the function virtual.
It's also good practice to explicitly mark the derived classes destructor as override.
Number 15: thinking that class members are initialized in the order they appear in the initializer list.
Reading left to right this looks fine.
The actual order that members are initialized in is the order that they're declared in.
First, we initialize end as start plus size but this m start is garbage. It hasn't been initialized yet.
We can of course fix this by declaring start first instead.
Or, since start is also a parameter to the function, we could just define the end variable in terms of the start parameter.
Number 16: not realizing there's a difference between default and value initialization.
x and x2 are default initialized.
They contain garbage until you put something into them.
y, y2 and y3 are value initialized.
They are guaranteed to contain the value zero.
z, on the other hand, is neither default nor value initialized.
This is a function declaration.
Even if it's a tiny bit less efficient, initializing your values is almost always a good idea.
The same kind of thing goes if you have an aggregate or array type.
In the first two cases, n and m will be garbage and S will be the empty string.
In the last three cases, n and m are zero and S is the empty string.
Notice that, even with default initialization, the empty string did still get initialized.
That's because for both default and value initialization if you define a default constructor, it will be called.
Number 17: overuse of magic numbers.
Introducing a basic constant in your code can make it many times more readable.
The compiler is going to optimize it away anyway. Just give it a good name.
Number 18: attempting to add or remove elements from a container while looping over it.
Well, doing that is sometimes just necessary but what I mean is noobs often do it incorrectly.
We're trying to put a copy of the vector at the end of the vector.
Adding or removing an element to the vector may invalidate the iterators to the vector.
For example, push_back might need to resize the vector and move all the elements to a new location.
After moving the contents of the vector to a new location, you can't expect the end pointer to be the same.
You would run into the exact same issue.
In fact, it's probably clearer why you would run into this issue if you used iterators directly.
This is a case where using a loop index actually does solve the problem.
It doesn't matter if the contents of your vector get moved somewhere else the ith element is still the ith element.
Number 19: returning a moved local variable.
I totally get where you're coming from.
A vector can be a large object and you don't want to make a copy of it.
So, you go ahead and try to move it out.
If you had just tried to return v directly, there would have been no copy and no move.
In this situation, that's because of return value optimization.
But what if the compiler can't do return value optimization.
In all cases, the move is unnecessary.
The compiler always knows that it can move a local variable.
But in some cases, this actively prevents return value optimization.
So, that's why this is one of the few rules where I can say you should just never do this.
Which brings us to number 20: thinking that move actually moves something.
Here is an implementation of standard move.
The full templated definition might be a bit much to take in all at once.
So, let's take a look just at the int case.
Move takes in an int lvalue reference,
static casts it to an rvalue reference and returns it.
The exact same thing happens in the or value overload.
It just static casts to an rvalue and returns it.
A more accurate name for move is probably something like cast to rvalue.
Number 21: thinking that evaluation order is guaranteed to be left to right.
Here's a famous example.
We have a string that says, "but i have heard it works even if you don't believe in it".
We replace the first four characters with the empty string.
Then, we find even and replace it with only.
Then, we find don't and delete it.
With this reasoning, we should end up with: "i have heard it works only if you believe in it".
But prior to C++ 17, the compiler is actually allowed to compute any sub-expression in any order.
So theoretically, it could find the location of even first
and then replace the first four characters making that location off by four.
So, then when the second replace happens it would replace these four characters with only
and you can see how this goes on you don't get the result you expected.
Well, the good news is that as of C++ 17, this example is guaranteed.
If you have a.b, then a is guaranteed to be evaluated before b is.
However, even in C++ 20, the order that function arguments are evaluated is still not guaranteed left to right.
This wouldn't matter much if a b and c were pure functions.
But, if a b and c have side effects, then the order that they're called in might actually make a difference.
Number 22: using totally unnecessary heap allocations when a stack allocation would have been fine
Here, we create two customer records on the heap.
Then, we do some work and then we end up deleting those variables at the end of the function.
The question we should ask ourselves is: Did this really need to be a heap allocation?
There's a good chance it would have been fine if we just stack-allocated them.
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.
That brings us to number 23: not using unique pointer and shared pointer to do your heap allocations.
What happens if an exception gets thrown in the middle here?
Then, these deletes never occur and the memory is leaked.
When you want to make sure that a resource is cleaned up, you need to put that cleanup code in a destructor.
So, why don't we make a class that holds a pointer, and then in its destructor it deletes that pointer.
Well, that's exactly what unique_ptr does.
You can give it a heap-allocated pointer and when it goes out of scope, it deletes it.
A shared pointer on the other hand uses a reference-counting scheme, similar to what you might have in a language like Python.
When the reference count hits zero as the last shared pointer goes out of scope that shared pointer is in charge of the deletion.
This scheme is much more expensive because reference incrementing and decrementing have to be done atomically.
That brings us to number 24: constructing a unique or shared pointer directly instead of using make unique or make shared.
make_unique and make_shared will pass your arguments directly to the constructor of your object.
Number 25: any use of new or delete.
There's no reason to rewrite functionality that already exists.
Here, I'm trying to manage the memory of some resource and then delete it when it's done.
That's already what a unique pointer does.
Don't try to couple the purpose of your class to the idea of ownership of an object.
That's a completely separate issue.
Unique pointer and shared pointer together cover pretty much every valid use of new or delete.
Number 26: any kind of attempt to do manual resource management.
This is pretty much the same story as with new and delete.
If you find yourself manually freeing closing or releasing any kind of resource,
then look to see if there's a class that does that automatically.
By the way, this idea of having resources automatically close in a destructor is called RAII.
It stands for Resource Acquisition Is Initialization.
But it really has more to do with ensuring that resources are released upon destruction.
Number 27: thinking that raw pointers are somehow bad.
Here's a basic max function.
This function is just reading from the pointers.
It doesn't care at all who's in charge of deleting them.
If your function doesn't have anything to do with the ownership of the objects in question,
then there's no need to use smart pointers.
The convention is that raw pointers don't own what they're pointing to.
And this should not be confused with the const-ness of the pointer.
This add function adds the source into the destination.
But this function is not in charge of the lifetime of either of the pointers.
Of course, if you're interoperating with C code, C code is not going to share this convention.
A C-function might very well return some heap-allocated memory to you that it expects you to delete.
Be careful though. If you've got a pointer with malloc, you need to delete it with free.
You can't just let unique pointer try to delete it with the built-in delete.
You can still use a unique pointer though.
You just need to define your own deleter which uses free.
Number 28: returning a shared pointer when you aren't sure the object is going to be shared.
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.
They could even directly assign the unique pointer return value to a shared pointer.
But if you return to them a shared pointer in the first place,
the damage would already be done if all they really wanted was a unique pointer.
Number 29: thinking that shared pointer is thread-safe.
The reference counting part of the shared pointer is thread-safe.
Here, I make a shared resource and pass it off to two worker threads.
Because the reference counting is thread-safe,
there's no danger that the object will fail to be deleted or be deleted twice.
However, it's only the reference counting part that's atomic.
This part here where we're accessing the x variable and the resource is not atomic and there's no locks.
This is a plain old data race of two threads trying to modify the same memory.
If you want to fix the data race, you need to fix it the way you'd fix any other data race.
And 30: confusing a const pointer with a pointer to const.
The concept of a const pointer versus pointer to const is pretty simple.
But, a lot of newbies struggle to remember how to tell the difference between them syntactically.
The rule is that const applies to whatever is immediately to its left.
Unless it's the leftmost thing, in which it applies to the thing to its right.
So here, the cost applies to the int, not to the pointer.
Here, the const applies to the int, not to the pointer.
And here, const applies to the pointer, not to the int.
Number 31: ignoring compiler warnings.
Ignoring them or turning them off very frequently leads to undefined behavior.
I hope you enjoyed my list of newbie C++ habits.
As always thank you to my patrons and donors for supporting me.
See you next time.