Why I like C++ attributes

Attributes are an underrated feature of the C++ language, in my opinion. I am saying this because I rarely see attributes used in code or samples featured in articles, videos, or talks. Although some of the standard attributes are targeted towards library implementers or address a limited number of scenarios (such as [[no_unique_address]], [[noreturn]], or [[carries_dependency]]), there are several that are quite useful in many situations. I refer here to [[nodiscard]], [[maybe_unused]], and [[deprecated]], which are the attributes I will talk about in this post.

This article was actually inspired from a recent situation I encountered while refactoring some old code, when, obviously, I introduced a bug. Using one of these attributes would have helped me avoid the problem. But now, my refactor code does contain attributes. But let me show the problem. The following snippet is a greatly simplified version of my old code. There is a class, called component, that adds new rows to a database table, in different ways. In the code shown here, if the record is already present, it is skipped for insertion. This happens in the append_row() function which first checks if the record exist in the database, and if it does not exist, it adds it.

enum class result {ok, no_data, error};

struct row {};
struct database
{
   result add(row r)
   {
      return result::ok;
   }
};

struct component
{
   result find_row(row r)
   {
      return result::no_data;
   }
   
   result append_row(row r)
   {
      result res = find_row(r);
      if (res == result::ok)
      {
         skipped_count++;
      }
      else
      {
         res = db.add(r);
         if (res == result::ok)
            added_count++;
         else
            error_count++;
      }

      return res;
   }
};

int main()
{
   component c;
   row r;

   c.append_row(r);
}

Because the actual class was larger and the code for adding a record was needed in several places and it was duplicated, I decided to create a function for this purpose that looked like this:

result add_row(row r)
{
   result res = db.add(r);
   if (res == result::ok)
      added_count++;
   else
      error_count++;
   return res;
}

Having this new add_row() function, I refactored my initial append_row().

result append_row(row r)
{
   result res = find_row(r);
   if (res == result::ok)
   {
      skipped_count++;
   }
   else
   {
      add_row(r);
   }

   return res;
}

If you paid attention to what I did here perhaps you spotted the error I did without realizing. I just called add_row(r) but ignored its return value. As a result, the return value from append_row() depends only on the execution of find_row(). Of course, that blew up at some point in my code and it took me a little time to debug and find the error.

The function needs to look like this:

result append_row(row r)
{
   result res = find_row(r);
   if (res == result::ok)
   {
      skipped_count++;
   }
   else
   {
      res = add_row(r);
   }

   return res;
}

So how do attributes help with this problem? The [[nodiscard]] attribute tells the compiler that the return value from a function should not be ignored, and if it is, a warning should be issued. In C++20, there is a new version of the attribute that takes a literal string that the compiler should display within the warning message (such as [[nodiscard("check if not null")]]). The [[nodiscard]] attribute can appear in a function, enumeration, or class declaration.

In my case, the [[nodiscard]] attribute was useful on the add_row() function. The following is the same function marked with the attribute:

[[nodiscard]]
result add_row(row r)
{
   result res = db.add(r);
   if (res == result::ok)
      added_count++;
   else
      error_count++;
   return res;
}

As a result, calling add_row(r) and ignoring its return value would result in the following compiler warning:

  • VC++: warning C4834: discarding return value of function with ‘nodiscard’ attribute
  • gcc: warning: ignoring return value of ‘result component::add_row(row)’, declared with attribute ‘nodiscard’ [-Wunused-result]
  • Clang: warning: ignoring return value of function declared with ‘nodiscard’ attribute [-Wunused-result]

Had I used the [[nodiscard]] attribute in the first place, a compiler warning would have helped me identify the bug immediately and avoid wasting time for debugging the problem.

This problem has an alternative fix. The [[nodiscard]] attribute could also be placed on the declaration of the result enumeration.

enum class [[nodiscard]] result {ok, no_data, error};

The implication is that the return value of any function that returns result cannot be ignored anymore. In our example, find_row(), add_row(), and append_row() all return a result value, therefore none of these calls can have their result ignored. That means we must change the main() function as follows:

int main()
{
   component c;
   row r;

   result res = c.append_row(r);
}

Now, the return value from append_row() is no longer discarded. However, the variable res is not used. That can lead to another warning from the compiler (depending on the compiling options):

  • VC++: warning: unused variable ‘res’ [-Wunused-variable]
  • gcc: warning C4834: discarding return value of function with ‘nodiscard’ attribute
  • Clang: warning: unused variable ‘res’ [-Wunused-variable]

To avoid this warning, another attributes should be used: [[maybe_unused]]. The effect of this attribute is that the compiler will suppress warnings on unused entities. It can appear on the declaration of functions, classes, structs, unions, enumerations and enumerators, variables, static and non-static data members, and typedefs.

The code in main() should change as follows to suppress the aforementioned warning:

int main()
{
   component c;
   row r;

   [[maybe_unused]] result res = c.append_row(r);
}

Another example for using the [[maybe_unused]] attribute is to suppress warnings for unused parameters of functions. For instance, the find_row() function does not use it’s r parameter, so a similar warning of unused parameter is used. Here it is how you can silence this warning:

result find_row([[maybe_unused]] row r)
{
   return result::no_data;
}

All code evolves over time, and sometimes we need to do refactoring, sometimes we need to change how some things work, or add new functionalities. When you build public APIs that are used by other people you can’t introduce any breaking change you want because that will limit the number of people that will use new versions of your library or framework. You often need to provide backwards compatibility but, at the same time, discourage the use of some old APIs in favor of new ones. This is exactly what the third attribute I mentioned in the beginning, [[deprecated]], is doing.

When the compiler encounters an entity marked with this attribute it issues (typically) a warning. The attribute [[deprecated]] also has a form that allows to specify a string literal that is supposed to indicate what is the reason for deprecating the entity and what should be used instead (such as [[deprecated("use smarter_find_row()")]] shown below). This string is used by the compiler when displaying the warning message. This attribute can be used in the declaration of functions, namespaces, classes, structs, unions, enumerations and enumerators, variables, static and non-static data members, template specializations, and typedefs.

Let’s suppose that in our example above, we provide a new implementation of the find_row() function. Let’s call it smarter_find_row() and we want everybody to use this new function. We can deprecate the old one using the [[deprecated]] attribute. This is shown here:

[[deprecated("use smarter_find_row()")]]
result find_row(row r)
{
   return result::no_data;
}

result smarter_find_row(row r)
{
   return result::no_data;
}

Since this function is called in append_row() we now get a warning, if using gcc or Clang, and an error, if using VC++:

  • VC++: error C4996: ‘component::find_row’: use smarter_find_row()
  • gcc: warning: ‘result component::find_row(row)’ is deprecated: use smarter_find_row() [-Wdeprecated-declarations]
  • Clang: warning: ‘find_row’ is deprecated: use smarter_find_row() [-Wdeprecated-declarations]

The reason VC++ issued an error is that, by default, it has another option enabled. That is /sdl, which enables additional security checks. This has the result of turning the C4996 warning into an error. If you disable this option (compile with /sdl-) then you get a warning for using a deprecated function. You can make this change if you go to Project Properties | Configuration Properties | C/C++ | General and change the selection for SDL Checks.

When you use this attribute with variables or functions, you need to put it in front of the type. However, for other entities, such as classes, enumerations, namespaces, or template specializations, the attribute must precede the name and not the keyword that declares it. Here is an example for classes:

struct [[deprecated("use nosql_database")]] database
{
   result add(row r)
   {
      return result::ok;
   }
};

On the other hand, for enumerators, the attribute must succeed the name, as shown in the following example:

enum class [[nodiscard]] result
{ 
   ok = 0, 
   no_data [[deprecated("use not_found")]] = 1, 
   error = 2,
   not_found = 3 
};

Attributes are a very useful feature and C++ developers, including myself, should use them more. They help us to understand the code better and the compiler to help us to write better code. This is why I like them and I plan to use them more often.

1 Reply to “Why I like C++ attributes”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.