Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

The Most Essential C++ Advice

4.84/5 (58 votes)
2 Jan 2023CPOL8 min read 105.4K  
Short list of things to watch out for when using C++
Rather than a long and complete guide with comprehensive explanations, this is a short list of things to watch out for when using C++, kept up to date based on the things I see in code reviews.

Introduction

There are huge textbooks, hours of lecture video, lengthy “style” guides, not to mention the content of sites like this one. But, I was asked for a terse set of bullet points, aimed at C++ programmers starting a new project.

Now I like to maintain a hot list based on the actual code being produced by the team I’m working with. Rather than a long and complete guide with comprehensive explanations, this is a short list of things to watch out for, kept up to date based on the things I see in code reviews.

But how can I make a hot list for unknown people with various skill levels with differing established habits? The key is to look at the big picture. Don’t dwell on specific details, but take a look at the meta issues. In order to keep this paper short and succinct, it does not contain extensive examples. Rather, links are included and proper terminology is used so you can look up topics you need to learn about. Consider this to be a syllabus, not a textbook. It will help you with a checklist of self-review issues, and inform you of topics you need to learn.

Documentation You Should Bookmark

C++ Core Guidelines

Have you heard of the Standard Guidelines Library and the C++ Core Guidelines? This is a set of guidelines maintained by Stroustrup himself, and informed by the top people in the community. It states:

Quote:

The guidelines are focused on relatively high-level issues,... Following the rules will lead to code that is statically type safe, has no resource leaks, and catches many more programming logic errors than is common in code today. And it will run fast -- you can afford to do things right.

C++ Reference

The website cppreference.com is the place to look up details on standard library classes. It’s also useful for basic information on specific language features.

Know Your Libraries

Standard library, other (e.g., Boost), frameworks, and project-specific.

Don’t rewrite the same function over and over.

Important Low-Hanging Fruit

No #define

Don’t use the preprocessor for defining entities that can be expressed in C++. They should be used only for things that require a text-based preprocessor and transcend the language: e.g., conditional compilation and abstracting compiler-specific extensions. Even then, beware that they might not work well with Modules (coming soon, as I write this).

Simple value constants should be defined using const / constexpr. Groups of related values might use an enumeration (enum) type.

Function-like macros should be replaced with inline functions. If the macro is used with different types, make it a template.

C++
#define PORT 254   // replace with...
constexpr int PORT = 254;

#define foo(x,y) ((x)+(y))/((x)-(y))   // replace with...
template <typename T>
T foo (T x, T y) { return (x+y)/(x-y); }

Declare Variables Where Needed, and Initialize Them

The C programming language originally (prior to C99) required variables to be declared first, before any code. Many programmers learned it this way and passed it on for generations. It is much better to declare variables in the smallest scope needed, and not until you are ready to actually use it. This is such an improvement that the C language adopted it more than twenty years ago!

Declare variables with an initializer. And of course, you can’t do that until you know what the initial value will be. Variables are not penguins huddling together to keep warm.

Virtual Function Marking

A declaration for a virtual function should use exactly one of virtual, override, or final keywords.

See Core Guidelines C.128.

Prior to C++11, virtual functions in hierarchies proved to be a maintenance hazard. If you change the signature of a virtual function, and don’t change every version of that virtual function, the compiler does not give any error, but those unchanged functions no longer override the intended virtual function. Now, you declare that a function is intended to override an existing virtual function from a base class, and the compiler will give an error if it can’t find a match.

Use const

Some people, familiar with certain other languages, forget that const is a thing. Be liberal using const to define variables. Pass large objects as const &.

Member functions that are accessors should be declared const.

DRY — Don’t Repeat Yourself

  • No copy/paste/paste/paste

Don’t use new/delete

See Core Guidelines R.11 and ES.60.

Don’t write ad-hoc code that does what std::vector, std::string, or other standard containers do for you.

If you need pointers, use std::unique_ptr or std::shared_ptr. Allocate by calling make_unique / make_shared, not by writing a new-expression. Freeing is done automatically. See also Core Guidelines C.150 and C.151.

Don’t Use Old-Style or C-Style Casts

See Core Guidelines ES.48 and ES.49.

C++
double f = 3.14;
unsigned int n1 = (unsigned int)f; // C-style cast
unsigned int n2 = unsigned(f);     // functional cast

Generally, an explicit type conversion (either notation) defeats the normal type checking of the language. Furthermore, it does so in a "wild" way, performing different operations depending on the specific types involved. Consider a cast like (C*)p that is meant to simply remove the const-ness from the pointer p. Now, the type of p is changed to something else; this cast now means something totally different—it will reinterpret the address to be an unrelated type, causing all sorts of memory corruption before the program eventually crashes.

Using the named casting operators, you specify the kind of operation to be performed. Had the above example used const_cast<C*>(p) instead, the original code has the identical effect. But when the definition of p is changed, the compiler now gives an error. (Of course, it would be even better if you didn’t have to cast away const in the first place.)

Some casts can do things that an implicit conversion would do as well as additional things, like the reverse of the implicit conversion. So, when something could be done without a cast at all, don’t use one. That same cast, after changes to the program, could then indicate a different, undesired, operation rather than being an error. If you want to name the target type for clarity or to disambiguate a call, use a temporary variable instead.

C++
foo ((C*)p);  // avoid

C* p_as_base = p;  // this is an implicit conversion
foo (p_as_base);

Consider any old-style or C-style casts in your code to be a code review issue. Upon considering it, eliminate the cast or use a named casting operator if a cast is indeed needed.

Write (Good) Functions

Regular Types

See "Regular Types and why do I care?" presentation by Victor Ciura at Meeting C++ 2018. A conceptual summary would be that types should behave like the built-in types; i.e., do as the ints do.

Rule of 3/5/0

►Three: If you implement any of the basic housekeeping operations:

  • destructor
  • copy constructor
  • copy assignment operator

then you probably need to implement all of them.

If that is not the case, you should make that explicit and always declare them (as =default) anyway.

►Five: C++11 added two more special housekeeping operations:

  • move constructor
  • move assignment operator

If you don’t define any of the special housekeeping operations, then the move semantics are generated automatically based on the move semantics of the members, if that is supported by all members. But if you define any of the housekeeping operations, the compiler no longer assumes it knows what is going on, and will not auto-generate them. If you can support move semantics, define those functions; if the built-in logic does in fact work for you, define it as =default. If you want to disable move semantics or simply make it clear that you know they won’t work rather than just forgetting about them, define them as =delete.

►Zero: If you don’t need to define the housekeeping operations, don’t. When you write a class, the essential housekeeping operations (copy constructor, assignment, destructor) are all generated automatically to do that operation on all of the data members. If your object is composed of other properly-defined objects, it should all be automatic. It is only special constraints unique to this new type, or relationships between the members, that should cause you to specify housekeeping details for this class.

If you find yourself adding a destructor (or other housekeeping operation) solely due to individual data members, then that should be handled by that member instead. For example, if you have a raw pointer and the destructor deletes it, you should use a smart pointer instead.

If the default constructor merely supplies (non-default) values for some data members, you can write in-line member initializers instead and not explicitly define the constructor.

Value Semantics

Many other object-oriented languages use reference semantics. Programmers coming from other languages might write functions like:

C++
void do_something (string name) { ... }

Calling this will cause the string parameter to be copied, which is usually not what is intended.  Usually, you can pass objects by reference by explicitly using the & qualifier. In this case, the function should be written:

C++
void do_something (const string& name) { ... }

Part of being a regular object is having value semantics. See the section above on Regular Types.

You should return values using the function return mechanism. Avoid "out" parameters, and don’t come up with ad-hoc mechanisms to avoid returning by value! Due to compiler optimizations (RVO and NRVO), copy elision, and move semantics, simply returning a value from a function is quite efficient.

C++
std::vector<std::string> results = process_file (filename);

Because functions return values, you can initialize variables. More generally, value-bearing functions can be composed in an expressive manner, when functions supplying "out" parameters cannot.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)