Modern C++ Attributes Guide: C++11 to C++20
At some point in your C++ journey, you’ve definitely written (void)x;, forgotten a break, or ignored a return value and hoped for the best.
Most C++ bugs aren’t fancy template metaprogramming disasters — they’re boring mistakes.
Modern C++ has quietly added some really nice tools that help you write safer, clearer code without changing how your program behaves. They live inside [[double brackets]], and most people barely talk about them.
Over the years, I’ve seen more bugs come from ignored return values and accidental fallthroughs than from complicated templates. Modern C++ attributes solve exactly those problems — cleanly and portably.
These attributes are like comments the compiler actually reads — and in this post, we’ll cover the ones that make your code safer, cleaner, and faster.
C++17 [[fallthrough]] Attribute
In a switch statement, normally each case ends with break. If you omit break, execution continues into the next case. This behavior is called fallthrough.
Consider this role-based permission system:
switch (role) {
case Role::Admin:
enableAdminPanel();
enableEditing();
enableReadOnly();
break;
case Role::Editor:
enableEditing();
enableReadOnly();
break;
case Role::Viewer:
enableReadOnly();
break;
default:
denyAccess();
}This approach has several issues:
- Code duplication —
enableEditing()andenableReadOnly()are repeated, making the code harder to maintain. - Bug-prone — If you later modify
enableReadOnly(), you might forget to update all cases. - Obscures hierarchy — The permission hierarchy isn’t obvious:
Admin ⟶ Editor ⟶ ViewerWe can remove the break statements and reorder the cases to share behavior:
switch (role) {
case Role::Admin:
enableAdminPanel();
case Role::Editor:
enableEditing();
case Role::Viewer:
enableReadOnly();
break;
default:
denyAccess();
}This works, but it has problems:
- The compiler will warn: “possible fallthrough”
- Future readers won’t know if this was intentional or a bug
C++17 introduced the [[fallthrough]] attribute to mark intentional fallthrough:
switch (role) {
case Role::Admin:
enableAdminPanel();
[[fallthrough]];
case Role::Editor:
enableEditing();
[[fallthrough]];
case Role::Viewer:
enableReadOnly();
break;
default:
denyAccess();
}It has No code duplication — Each permission is defined once
It shows clear intent — The [[fallthrough]] attribute tells the compiler and readers: "This is intentional, not a bug"
Better Expresses hierarchy — Higher roles inherit lower permissions naturally
No compiler warnings — The compiler knows you did this on purpose
[[fallthrough]]tells everyone—compiler and humans alike—"I did this on purpose. It's not a bug."
Use[[fallthrough]]when you think: "This case should continue into the next one—on purpose."
Important:
- It can only be used it inside
switchstatements. It works nowhere else. - Place it right before the next
caseordefault .It’s placed wherebreakwould normally go (but you want to fall through instead) - You should only use it when fallthrough is actually intentional
Don’t use it when control can’t fall through:
case 1:
return;
[[fallthrough]]; // illegal - we already returnedDon’t attach it to statements:
[[fallthrough]] foo(); // invalid - it's not a statement modifierC++17 [[nodiscard]] Attribute
[[nodiscard]] tells the compiler: "Don't ignore the return value of this function or type."
If you ignore it, you get a compiler warning — because ignoring it is probably a bug.
Consider this file-saving function:
bool writeToDisk();
void save() {
writeToDisk(); // return value ignored
std::cout << "Saved!\n";
}If writeToDisk() fails and returns false, the program still prints "Saved!"
- The bug goes unnoticed
- Data might be corrupted or lost
- Users think everything worked fine
If we mark the function with [[nodiscard]]:
[[nodiscard]] bool writeToDisk();
void save() {
writeToDisk(); // compiler warning: ignoring return value
std::cout << "Saved!\n";
}Now the compiler warns you: “You’re ignoring a return value you shouldn’t ignore.”
So now you can fix it properly:
void save() {
if (!writeToDisk()) {
std::cerr << "Error: Failed to save!\n";
return;
}
std::cout << "Saved!\n";
}You can also apply it to entire types. See the below example:
struct [[nodiscard]] Result {
bool success;
std::string message;
};
Result doSomething();
void foo() {
doSomething(); // warning: ignoring return value
}This is useful for wrapper types like std::optional, std::expected, or custom result types.
C++20 enhanced this by letting you provide a custom message:
[[nodiscard("Check if the file was written successfully")]]
bool writeToDisk();The compiler shows your message in the warning, making it even clearer why the return value matters.
Even though its useful, Don’t overuse it. Skip it when:
- The return value is genuinely optional (e.g., logging functions)
- Ignoring the result is a valid use case
- The function primarily has side effects
// Don't do this
[[nodiscard]] void print(std::string_view msg); // void return anyway
// This is fine without [[nodiscard]]
int printf(const char* fmt, ...); // ignoring return is commonNote:
Use it for functions where ignoring the return is likely a bug.
Error codes, validation results, resource handles.Use it for types that should always be checked
Result wrappers, optional types, smart pointers.Add a reason string when the intent isn’t obvious (C++20), helps future maintainers understand why.
Don’t use it on
voidfunctions. They have no return value to discard.Don’t use it when ignoring the return is valid. Some functions return extra info that’s not always needed.
Simply speaking:
Use
[[nodiscard]]when you think: "If someone ignores this return value, they're probably making a mistake."
C++17 [[maybe_unused]] Attribute
[[maybe_unused]] tells the compiler:
This variable / parameter / function might not be used — and that’s OK.
Get Sagar’s stories in your inbox
Join Medium for free to get updates from this writer.
Take for example the below code, where the parameter tag is not needed to use for now:
void log(int level, const std::string& tag, std::string& message) {
std::cout << level << ": " << message << "\n";
}If you compile this code the compiler will give you warning the parameter tag is not used.
To fix this compiler warning the traditional way is just to cast it to void like below:
void log(int level, const std::string& tag, std::string& message) {
(void)tag;
std::cout << level << ": " << message << "\n";
}Pretend you used tag so that compiler will not give any more warning.
Its not that great approach and looks hacky. Instead of this we can use [[maybe_unused]] that clearly shows our intention to both the compiler and the programer that the paramter tag is not going to be used and its intentional.
void log(int level, [[maybe_unused]] const std::string& tag, std::string& message) {
std::cout << level << ": " << message << "\n";
}You can apply [[maybe_unused]] attribute to functions, parameters, variables, class members, enums, typedefs and also lambdas.
If it’s something you declare, you can use
[[maybe_unused]]on it.
Cannot be applied in some cases as well:
- It only applies to entities, not expressions or statements.
[[maybe_unused]] x = 10; // invalid- Not allowed on blocks or scopes
[[maybe_unused]] { // invalid
int x = 5;
}- You can’t attach it to control keywords like
if,for,return.
If it’s not a declared thing, you can’t put
[[maybe_unused]]on it.
What if you mark something [[maybe_unused]] but actually use it?
The answer is Nothing bad happens. Your code compiles and works normally. It won’t break your code — but it weakens your compiler warnings, which is the real cost.
[[maybe_unused]] int x = 10;
std::cout << x; // perfectly fineC++14 [[deprecated]] Attribute
[[deprecated]] tells the compiler:
“Don’t use this anymore — there’s a better alternative.
When someone uses a function or variable that is marked as depricated, the compiler will give warning.
[[deprecated]] void oldFunc() {}
void f() {
oldFunc(); // warning: deprecated
}Also you can add a message to it, which is really helpful.
[[deprecated("Use newFunc() instead")]]
void oldFunc() {}When compiled, compiler give warning with your custom message:
warning: 'oldFunc' is deprecated: Use newFunc() instead
[[deprecated]]= “This still works, but stop using it — it will go away.”
This can be applied to functions, variables, classes and also structures.
// Depricating an entire struct
[[deprecated("Use NewConfig instead")]]
struct OldConfig {
int x;
};
// Depricating a constructor
struct Logger {
[[deprecated("Use Logger(int level) instead")]]
Logger() {}
Logger(int level) {}
};
// Depricating just a function
struct Logger {
[[deprecated("Use write() instead")]]
void log() {}
void write() {}
};Put
[[deprecated]]directly on the specific constructor or function you want users to stop using — not the whole class if not necessary.
Unknown Attribute Handling
Before C++17: If a compiler saw an unknown attribute, it might error out
Since C++17:
- Compilers must ignore unknown attributes
- They may warn, but compilation must continue
Take for example the below code.In C++17, if your compiler doesn’t know vendor::special_opt, it will just ignore it — your code still compiles.
[[vendor::special_opt]] int x = 10;- This makes code portable across GCC, Clang, MSVC
- This lets libraries use vendor attributes safely
- By this we can avoid
#ifdefclutter
C++17 made attributes safe to ignore, so unknown ones won’t break your build anymore.
C++20 [[likely]] and [[unlikely]] Attributes
The attribute [[likely]] and [[unlikely]] introduced in C++20 tells the compiler:
[[likely]] : This path usually happens.
[[unlikely]]: This path almost never happens.
These attributes give the compiler a hint about which branch is more likely to run, helping with performance optimizations (like better branch prediction). They don’t change logic — only help the compiler make faster code.
The compiler uses this information to optimize code layout for better branch prediction and instruction caching, potentially improving performance.
For example:
if (!ptr) [[unlikely]] {
return -1; // rare error
}
process(*ptr); // common caseThis means, most of the time ptr is valid, so optimize that path.
Follow these Rules:
Use
[[unlikely]]for error paths: Errors, validation failures, null checks — these are typically rare.Use
[[likely]]for the common case in hot code: Performance-critical loops, frequently-hit cache paths.Only use when you actually know the branch frequency: Don’t guess. Profile your code first.
Don’t use it everywhere: Overusing it can hurt performance if you’re wrong about the probabilities.
Don’t use it for 50/50 branches: If both branches are equally likely, these attributes don’t help.
Skip using these attributes when:
- You don’t know which branch is more common
- Both branches are roughly equally likely
- The code isn’t performance-critical
- You haven’t profiled and confirmed the bottleneck
// Don't guess
if ([[unlikely]] userInput == "yes") { // Could be 50/50!
doSomething();
}
// Only use when you know
if ([[unlikely]] !fileExists(path)) { // Files usually exist
createFile(path);
}You can use [[likely]] and [[unlikely]] on any branch.
By these attribute you’re just telling the compiler:“This path usually runs.” No matter where that path appears.
Simply putting it:
[[likely]]/[[unlikely]]= “This branch is hot. That one isn’t.
If you’re serious about passing technical interviews, try PracHub. It helped me structure my preparation with advanced mock sessions. [Check it out here] and start practicing. (Disclosure: This is an affiliate link).
Found this article helpful? Please Clap 👏 and follow for more C++ and system programming content.








