Attributes

Explore the fundamentals of attributes, including [[nodiscard]], [[likely]], and [[deprecated]]
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art showing a fantasy maid character
Ryan McCombe
Ryan McCombe
Updated

Attributes are small statements we can add to our code to give the compiler, or other developers, guidance. Attributes can be identified by the [[ and ]] that surround them.

There are many attributes we can use, and more are being added regularly. We cover some of the most useful ones in this lesson:

  • [[nodiscard]] indicates that we expect a function’s return value to be used. If it is discarded, the compiler will generate a warning
  • [[likely]] and [[unlikely]] within conditional logic give the compiler guidance over which code path we expect to be taken, allowing it to optimize around the likely path
  • [[deprecated]] gives other developers guidance that we’d prefer they not use a specific function or class, as we intend to remove it soon.

The [[nodiscard]] Attribute

Let's consider the following code:

int Add(int x, int y) {
  return x+y;
}

int main() {
  Add(1, 2);
}

This is simple and compiles as expected. However, if someone wrote code like this, they certainly made an error.

This is because the only thing our Add function does is return a value. Yet, when Add is called, that return value is being ignored.

So, the call to Add isn’t doing anything. The developer probably misunderstood how our function works and is about to introduce a bug, or may just be wasting resources.

If we annotate the Add function with a [[nodiscard]] attribute, our program still works as expected, but the compiler now helpfully warns us of the issue:

[[nodiscard]]
int Add(int x, int y) {
  return x+y;
}

int main() {
  Add(1, 2);
}
warning: Ignoring return value of function declared with 'nodiscard' attribute

Function Side Effects

Specifically, [[nodiscard]] is designed to prevent us from discarding the return value of a function that doesn’t have any side effects.

Side effects are things that the function does to modify the state of our program, beyond just returning a value. Some examples of side effects include:

  • Logging into the console
  • Modifying variables outside the scope of the function - this can include modifying arguments that were passed by reference
  • Calling some other function that has side effects

If a function has side effects, it isn’t appropriate to mark it as [[nodiscard]], as the caller may be calling the function just for those side effects.

The [[likely]] and [[unlikely]] Attributes

When we’re creating conditional logic, sometimes one of our branches is much more likely to be followed. When we know that, we can provide that insight to the compiler, using [[likely]]:

void StartQuest(Character* Player) {
  if (Player->IsAlive()) [[likely]] {
    // ...
  }
  // ...
}

Similarly, we can inform the compiler that a condition is unlikely to be true using the [[unlikely]] attribute:

void Resurrect(Character* Target) {
  if (Target->IsAlive()) [[unlikely]] {
    // ...
  }
  // ...
}

When the compiler knows a path is going to be very commonly used within our function, it can optimize the compilation of that function accordingly.

This gives a performance benefit. Additionally, other developers reading our code can get a sense of the expected context.

For example, reading this example, we know that it is possible that our Resurrect function could be given a Target that is already alive, but it’s not likely.

Profile-Guided Optimization (PGO)

A more advanced form of this technique commonly used on larger projects is profile-guided optimization or PGO.

Our program might have a function that is called hundreds or thousands of times per second, whilst another is rarely ever called. The compiler should arrange our code to optimize the performance of the function that is called thousands of times per second.

But the compiler doesn’t inherently know how often our functions will be called at runtime.

With PGO, we first compile our project with additional data collection enabled. We then run our program and use it as normal, whilst the data collection generates a detailed profile of what code paths are being executed.

This profile records things like how often each function is called, which branches are most commonly taken, and how often loops iterate.

Once we have enough data in our profile, we then stop running our program, and compile it again. This second compilation uses the profile we generated from running the program.

The compiler can then optimize around how our program is used, guided by the insights from this profile

The [[deprecated]] Attribute

The final attribute we want to cover is [[deprecated]]. It is common for us to want to change or remove a function.

But, that function may be used in hundreds or thousands of other places. This is particularly common when we’re working on a large project with colleagues, or working on a library that is being used in other projects.

Instead of attempting to remove it, it’s common to instead mark it as deprecated. This allows our function to still be used, but anyone using it will get a warning asking them to update their code.

Then, after some time, we can remove the function entirely, with less disruption. We can do this using the [[deprecated]] attribute:

[[deprecated]]
void StartQuest(Character* Player) {
  // ...
}

Now, code that calls this function will still work, but they will get a compiler warning:

'StartQuest' is deprecated

Generally, we will want to give more information - for example, what should the consumer use instead? We can update our [[deprecated]] attribute to include a message:

[[deprecated(
  "StartQuest(Character*) will be removed"
  "soon - switch to StartQuest(QuestJournal*)"
)]]
void StartQuest(Character* Player) {
  // ...
}

void StartQuest(QuestJournal* Journal) {
  // ...
}
warning C4996: 'StartQuest': StartQuest(Character*) will be removed soon - switch to StartQuest(QuestJournal*)

Visual Studio C4996 Error

By default, Visual Studio’s compiler treats invocations of a [[deprecated]] function as an error, rather than a warning. This prevents us from building our project.

We can change this by opening the Project menu from the top bar, and then visiting Properties > C/C++ > Command Line. On this panel, under Additional Options, we can add: /sdl /w34996 to have the compiler treat this error as a warning instead.

There are many more attributes we can use. Additionally, compilers often introduce their own, non-standard attributes, which we can include in our code if we’re using that compiler.

However, this summary introduced the most common and useful attributes.

Summary

In this lesson, we've explored the concept of attributes. The key things we learned included:

  • Learned the use of [[nodiscard]] to prevent discarding important return values and to catch potential errors.
  • Understood how [[likely]] and [[unlikely]] attributes guide the compiler for optimization by indicating the expected path in conditional logic.
  • Explored the [[deprecated]] attribute to manage the evolution of code by marking functions or entities for future removal.
  • Gained insights into the importance of compiler-guided optimization and how attributes contribute to efficient code execution.
  • Discussed the concept of function side effects and when it's appropriate to use certain attributes.

Preview of the Next Lesson

In the upcoming lesson, we delve into the world of randomness in C++. This allows us to introduce non-deterministic results in our program, which we demonstrate in the context of a combat system for a role-playing game. The key topics we’ll cover include**:**

  • Understanding Random Number Generation: Learn the basics of how random numbers are generated in C++ and their applications.
  • Seeding Random Number Generators: Understand how to seed a random number generator, including how to control the seed to allow for repeatable results.
  • Different Random Engines in C++: Understand the purpose of random number engines, like std::mt19937.
  • Distributions and Their Uses: Explore different types of distributions (like uniform, normal, etc.) and how they shape random data for various use cases.

Was this lesson useful?

Next Lesson

Random Number Generation

This lesson covers the basics of using randomness, with practical applications
3D art showing a character playing with dice
Ryan McCombe
Ryan McCombe
Updated
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
Clean Code
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 56 Lessons
  • Over 200 Quiz Questions
  • 95% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Random Number Generation

This lesson covers the basics of using randomness, with practical applications
3D art showing a character playing with dice
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved