C++ Core Check: safe coding guidelines update

For performance, more for safety

Rust and C++ are two more popular system-level development languages. Over the years, the industry’s focus on C++ has been mainly on performance, and we have constantly heard feedback from customers and security researchers: They hope that C++ should have more secure coding guidelines at the language level.
In terms of safe programming, C++ is often considered to lag behind Rust.
Drawing on the features of Rust in secure coding, we have added four coding security guidelines to the C++ Core Check of Visual Studio 2019 v16.7. Let's take a look.

The switch statement has no default label

The pattern matching structure in Rust is similar to the switch language structure in C++. The main difference is that Rust requires developers to cover all pattern matching possibilities, either by writing an explicit processor for each pattern, or adding a default processor (if all other patterns do not match) .

For example, the following Rust code will not compile because it lacks the default processor.

 

This is a concise security feature because it can prevent such programming errors that are easy to happen but not so easy to catch.

If the enumeration type is used in the switch statement and not every enumeration value is judged, Visual Studio will warn the developer and issue C4061 and C4062. However, for other types, such as integers, there is no such warning.

In this version, we have introduced a safe coding principle: For non-enumerated types (such as char, int), if there is no default processing label in the switch statement, Visual Studio will issue a warning. You can select three different rules in the rule settings of the project and then perform code analysis.
> C++ Core Check Style Rules
> C++ Core Check Rules
> Microsoft All Rules

Let's use C++ to rewrite the Rust example above.

 

If we remove the default label, Visual Studio will give the following warning:

 

Unannotated fallthrough in switch statement

Another limitation on pattern matching in Rust is that they do not support implicit jumps in case statements. In C++, the following code can pass the compiler check perfectly.

 

The above C++ code is very reasonable to open, but the implicit jump in the case statement can easily become a bug in the program. For example, if the developer forgets to add a break statement after each(food) call, the code will still be compiled, but the result of the operation will be very different. If the scale of the project is very large, it will be difficult to track such bugs.

Fortunately, C++17 added the annotation [[fallthrough]]. The main purpose is to implicitly jump in different case statements. In this way, in the above example, developers can use this annotation To show the compiler that he really wants to perform this behavior.

In Visual Studio 2019 v16.7, if an implicit jump occurs without the [[fallthrough]] annotation in the code, the compiler will give a C26819 warning. This rule is enabled by default when Visual Studio performs code analysis.

 

In order to solve the above warning, you can add the [[fallthrough]] annotation to the case statement, as shown in the following figure:

 

Expensive copy operation

One of the main differences between Rust and C++ is that Rust uses move semantics by default instead of copy.
for example:

 

This means that when you really need copy semantics, you need to use an explicit copy statement, as shown in the following figure:

 

C++ is different, it defaults to copy semantics. Generally speaking, this is not a big problem, but sometimes it may cause some bugs. A frequently occurring example is when using the range-for statement, let us look at this example:

 

In the above code, each primitive in the vector is copied into p in each iteration loop. If the element is a large structure, the copy operation will be very expensive, and this situation is not easy to see.
In order to avoid this kind of unnecessary copying, we have added a coding guideline in C++ Core Check. It is recommended that developers remove this copy operation, as shown in the following figure:

 

The following is a method to determine whether a certain copy operation is necessary:
If the size of the type is greater than twice the size of the platform-related pointer, and the type is not a smart pointer or one of gsl::span, gsl::string_span or std::string_view, Then the copy is considered unnecessary. This means that for smaller data types (such as integers), the warning will not be triggered. For larger types, such as the Person type in the example above, the copy operation is considered expensive (unnecessary) and the compiler will issue a warning.

The last point about this rule is that if the variable in the loop body is mutated, the warning will not be triggered, as shown in the following figure:

 

If the container used is not a const type, you can avoid unnecessary copying by modifying the object as a reference type.

 

However, this modification will cause a new side effect. Therefore, this warning only recommends marking the loop variable as a const reference. If the loop variable cannot be marked as const type legally, this warning will not be triggered.

This coding guideline is enabled by default.

Copy of auto type variable

The last check rule is about the copy operation of auto type variables.
Consider the following Rust code in which type resolution is performed for the variable to which the reference is assigned.

 

Due to the requirements of Rust, in most cases, the copy must be explicit. Therefore, in the above example, the password type will be automatically resolved into an immutable reference after the immutable reference is assigned, and expensive copy operations will not be performed.

On the other hand, consider the following C++ code:

 

In the above code, even if the return type of getPassword is a const reference to a string, the type of password will be parsed as std::string. As a result, the contents of PasswordManager::password are copied to the local variable password.

Here is a comparison with a function that returns a pointer:

 

The difference in behavior between assigning references and pointers to variables marked as auto is not obvious, which may lead to unnecessary and accidental copies.

To prevent errors due to this behavior, the checker checks all initialized instances from references to variables marked as auto. If the generated copy operation is deemed expensive using the same heuristics as range checking, the checker will issue a warning, suggesting that the variable be marked as a const reference type.

 

And like scope checking, as long as the variable cannot be legally marked as const, this warning will not be issued.

 

Another situation where no warning is issued is whenever a reference is derived from a temporary object. In this case, once the temporary file is destroyed, using a const auto reference will result in a "dangling" reference to the destroyed temporary variable.

 

This coding guideline is enabled by default.

to sum up

If you can read (write) here, I think it should be a man.
Some coding guidelines (for example, when declaring variables must be initialized), it is best to become your muscle memory. When writing a certain code structure, it is your muscles, not your brain, to complete the safe coding principles.

At last

The blog of the Microsoft Visual C++ team is one of my favorite blogs. It contains a lot of knowledge about Visual C++ and the latest developments. If you are still interested in the ancient technology of Visual C++, you can often visit them (or me).
This article is from: "New safety rules in C++ Core Check"

Guess you like

Origin blog.csdn.net/mmxida/article/details/108476335
Recommended