当前位置:网站首页>Tips for this week 140: constants: safety idioms

Tips for this week 140: constants: safety idioms

2022-07-07 17:58:00 -Flying crane-

As TotW#140 Originally published in 2017 year 11 month 8 Japan

from Matt Armstrong A literary creation

stay C++ What is the best way to express constants in ? You may know the meaning of this word in English , But it's easy to mistakenly express this concept in code . ad locum , We will first define some key concepts , Then list security technologies . For curious people , We will learn more about the possible problems later , And describe C++17 Language function , This function makes it easier to express constants .

“C++ Constant ” There is no formal definition , So let's propose an informal definition .

  1. value : The value never changes ; Five or five . When we want to express a constant , We need a value , And there is only one .
  2. object : At every point in time , Object has a value .C++ Great emphasis is placed on the variability of objects , But constant changes are not allowed .
  3. name : Naming constants is more useful than literal constants . Variables and functions can be evaluated as constant objects .

in summary , Let's call constants variables or functions that always evaluate to the same value . Here are a few key concepts .

  1. Secure initialization : Many times constants are expressed as values in static storage , Must be safely initialized . For more information , see also C++ Style guide .
  2. link : Links and how many instances of named objects are in the program ( or “ copy ”) of . Usually, it is better to have only one constant name for a single object in the program . For global or namespace scoped variables , This involves external links ( You can read more about links here ).https://en.cppreference.com/w/cpp/language/storage_duration
  3. Compile time evaluation : If you know the value of constants at compile time , Sometimes compilers can better optimize code . This benefit sometimes proves that it is reasonable to define the value of constants in the header file , Although it will increase complexity .

When we say we “ Add a constant ” when , We are actually declaring a API And define its implementation in a way that meets most or all of the above criteria . Language doesn't stipulate how we do this , Some methods are better than others . Usually the easiest way is to declare a const or constexpr Variable , If it is in the header file , Is marked as inline . Another way is to return a value from a function , This method is more flexible . We will introduce examples of these two methods .

About const Explanation : It is not enough . One const Object is read-only , But this does not mean that it is immutable , It doesn't mean that its value is always the same . The language offers changes that we think are const The value of the method , for example mutable Key words and const_cast. But even simple code can prove this :

void f(const std::string& s) {
    
  const int size = s.size();
  std::cout << size << '\n';
}

f("");  // Prints 0
f("foo");  // Prints 3

In the code above ,size It's a const Variable , But it saves multiple values while the program is running . It is not a constant .

Constant in header file

All idioms in this section are robust and worthy of recommendation .

An inline constexpr Variable

from C++17 Start , Variables can be marked inline , Make sure that there is only one copy of the variable . When and constexpr Used together to ensure safe initialization and destruction , This provides another way to define constants , The value of this constant can be accessed at compile time .

// in foo.h
inline constexpr int kMyNumber = 42;
inline constexpr absl::string_view kMyString = "Hello";

One extern const Variable

// Declared in foo.h
ABSL_CONST_INIT extern const int kMyNumber;
ABSL_CONST_INIT extern const char kMyString[];
ABSL_CONST_INIT extern const absl::string_view kMyStringView;

The above example declares an instance of each object . extern Keywords ensure external links . const Keywords help prevent unexpected mutations in values . This is a good method , Although it does mean that the compiler cannot “ notice ” Constant values . This limits their practicality to a certain extent , But it's not important for typical use cases . It also needs to be in the associated .cc Variables defined in file .

// Defined in foo.cc
const int kMyNumber = 42;
const char kMyString[] = "Hello";
const absl::string_view kMyStringView = "Hello";

ABSL_CONST_INIT Macros ensure that each constant is initialized at compile time , But that's it. . It does not make the variable const, Nor will it prevent the use of nontrivial destructors that violate the rules of the style guide to declare variables . See the reference to macros in the style guide .

You may want to use constexpr stay .cc Variables defined in file , But at present, this is not a portable method .

Be careful : absl::string_view Is a good way to declare string constants . This type has a constexpr Constructor and a normal destructor , Therefore, it is safe to declare them as global variables . Because the string view knows its length , So using them does not require runtime calls strlen().

One constexpr function

No parameters constexpr The function will always return the same value , So it is used as a constant , And can usually be used to initialize other constants at compile time . Because all constexpr Functions are implicitly inline , So there is no link problem . The main disadvantage of this method is that constexpr Limitations of code in functions . secondly ,constexpr yes API An important aspect of the contract , It has practical consequences .

// in foo.h
constexpr int MyNumber() {
     return 42; }

An ordinary function

When constexpr When the function is not desirable or feasible , You can choose ordinary functions . The function in the following example cannot be constexpr, Because it has a static variable :

inline absl::string_view MyString() {
    
  static constexpr char kHello[] = "Hello";
  return kHello;
}

Be careful : Make sure to use static... When returning array data constexpr specifier , for example char[] character string 、absl::string_view、absl::Span etc. , To avoid subtle mistakes .

A static class member

Suppose you are already using a class , Then static members of classes are a good choice . These always have external links .

// Declared in foo.h
class Foo {
    
 public:
  static constexpr int kMyNumber = 42;
  static constexpr char kMyHello[] = "Hello";
};

stay C++17 Before , Must also be in .cc The file provides definitions for these static data members , But for both static and constexpr Data members of , These are unnecessary now ( And has been discarded ).

// Defined in foo.cc, prior to C++17.
constexpr int Foo::kMyNumber;
constexpr char Foo::kMyHello[];

It's not worth introducing a class just to act as the scope of a bunch of constants . Please use other technology instead .

Discouraged alternatives

#define WHATEVER_VALUE 42

There is no reason to use a preprocessor , Please refer to the style guide .

enum : int {
     kMyNumber = 42 };

The enumeration technique used above is reasonable in some cases . It produces a constant kMyNumber, Will not lead to the problems discussed in this tip . But the alternatives already listed will be more familiar to most people , Therefore, it is usually preferred . Use enumeration when enumeration itself makes sense ( for example , See Tips #86 “ Use class enumeration ”).

The method that takes effect in the source file

All the above methods also apply to a single .cc file , But it may be too complicated . Because constants declared in the source file are only visible in that file by default ( See internal link rules ), So the simpler way , For example, definition constexpr Variable , Usually effective :

// within a .cc file!
constexpr int kBufferSize = 42;
constexpr char kBufferName[] = "example";
constexpr absl::string_view kOtherBufferName = "other example";

Above in .cc The document is very good , But not in the header file ( See warnings ). Read it again and remember it . I will explain the reason as soon as possible . Cut a long story short : stay .cc Variables defined in file constexpr Or declare them as extern const .

In the header file , Please note that

Unless you pay attention to the idioms explained above , otherwise const and constexpr Objects may be different objects in each translation unit .

It means :

  1. error : Any code that uses constant addresses will have errors , Even terrible “ No behavior defined ”.
  2. inflation : Each translation unit, including the title, has its own copy . It's no big deal for simple things like primitive numeric types . Not very good for strings and larger data structures .

Within the namespace ( That is, not in a function or class ) when ,const and constexpr Objects have implicit internal links ( The same links for unnamed namespace variables and static variables that are not in functions or classes ). C++ The standard guarantees that each translation unit that uses or references an object will get a different object “ copy ” or “ Instantiation ”, Each is located at a different address .

In a class , You must additionally declare these objects static , Otherwise, they will be instance variables that cannot be changed , Instead of unchangeable class variables shared between each instance of the class .

Again , In the function , You must declare these objects as static objects , Otherwise, they will take up stack space and construct each time the function is called .

An example error

that , Is this a real risk ? Please consider it :

// Declared in do_something.h
constexpr char kSpecial[] = "special";
// Does something. Pass kSpecial and it will do something special.
void DoSomething(const char* value);
// Defined in do_something.cc
void DoSomething(const char* value) {
    
  // Treat pointer equality to kSpecial as a sentinel.
  if (value == kSpecial) {
    
    // do something special
  } else {
    
    // do something boring
  }
}

Please note that , This code will kSpecial First of all char The address of value Compare as a magic value of a function . You sometimes see code doing this to shorten the complete string comparison .

This can lead to a subtle mistake . kSpecial An array is constexpr, This means that it is static ( have “ Inside ” link ). Although we think kSpecial yes “ A constant ”—— In fact, it's not —— It is a family of constants , One for each translation unit ! Yes DoSomething(kSpecial) The call to seems to do the same thing , But the function takes different code paths according to the location where the call takes place .

Any code that uses an array of constants defined in the header file , Or use the code of constant address defined in the header file , Is enough to solve this mistake . Such errors usually occur in string constants , Because they are the most common reason to define arrays in header files .

An example of undefined behavior

Just adjust the example above , And will DoSomething Move to the header file as an inline function . Bingo: Now we have undefined behavior , or UB. The language is required in each translation unit ( Source file ) All inline functions are defined in exactly the same way in —— This is the language “ A defining rule ” Part of . This particular DoSomething The implementation refers to a static variable , So each translation unit is actually right DoSomething The definition of is different , So it's undefined behavior .

Irrelevant changes to program code and compiler may change inline decisions , This may cause such undefined behavior to change from benign behavior to wrong .

Will this cause problems in practice ?

Yes . In one of the practical mistakes we encountered , The compiler can determine in a specific translation unit ( Source file ) in , Only the large static defined in the header file is partially used const Array . It doesn't emit the entire array , Instead, it optimizes the unused parts it knows . One way to partially use arrays is through inline functions declared in headers .

The problem is , This array is used by other translation units in this way , Static state const Arrays are fully used . For those translation units , The compiler generates an inline function version that uses a full array .

Then the linker appears . The linker assumes that all instances of inline functions are the same , Because the single definition rule stipulates that they must be the same . It discards all copies of the function , But only one copy - That's a copy with a partially optimized array .

When the code uses a variable in a way that needs to know its address , This error may occur . The technical term in this regard is “ The use of ODR”. In modern times C++ It is difficult to prevent ODR Using variables , Especially if you pass these values to the template function ( As in the above example ).

These mistakes do happen , And it is not easy to find in testing or code review . It is worth sticking to safe idioms when defining constants .

Other common errors

error #1: Not constat Constant

Common in pointers :

const char* kStr = ...;
const Thing* kFoo = ...;

above kFoo Is a pointer to a constant , But the pointer itself is not a constant . You can assign it to him , Set to NULL, etc. .

//  Corrected 
const Thing* const kFoo = ...;
//  That 's fine 
constexpr const Thing* kFoo = ...;

error #2: Unusual quantity MyString()

Consider this code :

inline absl::string_view MyString() {
    
  return "Hello";  //  Each call may return different values 
}

The address of a string literal constant is allowed to change every time it is calculated , So the above method is a little wrong , Because it returns string_view Each call may have different .data() value . Although in many cases this will not be a problem , But it may lead to the above error .

Make MyString() constexpr It doesn't solve the problem , Because the language standard does not say that it does . One way to look at this is ,constexpr Function is just an inline function , It is allowed to execute at compile time when initializing constant values . At run time , It is no different from inline functions .

constexpr absl::string_view MyString() {
    
  return "Hello";  //  Each call may return different values 
}

To avoid this kind of mistake , Please use static constexpr To replace .

inline absl::string_view MyString() {
    
  static constexpr char kHello[] = "Hello";
  return kHello;
}

Rule of thumb : If your “ Constant ” It's an array type , Store it in the local static of the function before returning . This fixes its address .

error #3: Non portable code

There are some modern C++ The feature has not been supported by some mainstream compilers

  1. stay Clang and GCC in , above MyString Static in function constexpr char kHello[] Arrays can be static constexpr absl::string_view. But it won't be Microsoft Visual Studio Chinese compiler . If portability is to be considered , Please avoid using constexpr absl::string_view, Until we start from C++17 get std::string_view type .
inline absl::string_view MyString() {
    
  // Visual Studio refuses to compile this.
  static constexpr absl::string_view kHello = "Hello";
  return kHello;
}
  1. For extern const Variable , According to the standard C++, The following method of defining its value is valid , And in fact, it's better than ABSL_CONST_INIT preferable , But some compilers do not support this method .
//  It's defined in foo.cc --  however MSVC 19 I won't support it .
constexpr absl::string_view kOtherBufferName = "other example";

As .cc In file constexpr Variable solution , You can provide its value to other files through functions .

error #4: Constants that are not properly initialized

The style guide has some detailed rules , Designed to protect us from common problems related to runtime initialization of static and global variables . When the global variable X The initialization of refers to another global variable Y when , There will be fundamental problems . How do we determine Y It doesn't depend on X Value ? Loop initialization dependencies can easily occur in global variables , Especially those variables that we think are constants .

This in itself is a rather tricky area of language . The style guide is an authoritative reference .

Consider the above links to read . Focus on constant initialization , The initialization phase can be interpreted as :

  1. Zero initialization . This is how to initialize other uninitialized static variables to types “ zero ” value ( for example 0、0.0、‘\0’、null etc. ) Why .
const int kZero;  // this will be zero-initialized to 0
const int kLotsOfZeroes[5000];  // so will all of these

Please note that , Rely on zero initialization in C Quite popular in code , But in C++ Quite rare and niche . It is often clearer to assign explicit values to variables , Even if the value is zero , It makes us …
2. Constant initialization

const int kZero = 0;  // this will be constant-initialized to 0
const int kOne = 1;   // this will be constant-initialized to 1

“ Constant initialization ” and “ Zero initialization ” stay C++ Language standards are called “ initiate static ”. Both are always safe .
3. dynamic initialization

// This will be dynamically initialized at run-time to
// whatever ArbitraryFunction returns.
const int kArbitrary = ArbitraryFunction();

Dynamic initialization is where most problems occur . The style guide is in https://google.github.io/styleguide/cppguide.html#Static_and_Global_Variables Explained why .

Please note that ,Google C++ Documents such as style guides have always included dynamic initialization in “ initiate static ” In this broad category . “ static state ” The word applies to C++ Several different concepts in , This may cause confusion . “ initiate static ” Can be said “ Initialization of static variables ”, This can include runtime calculations ( dynamic initialization ). Language standards vary 、 Use the term in a narrower sense “ initiate static ”: Initialization done statically or at compile time .

Initialize memo

This is an ultra fast constant initialization memo ( Not in header file ):

constexpr Ensure safe constant initialization and safe ( Insignificant ) damage . whatever constexpr Variable in .cc When defined in the file, it is completely OK , But for the reasons explained above , There is a problem in the header file .
ABSL_CONST_INIT Secure constant initialization . And constexpr Different , It doesn't actually make the variable const, Nor is it guaranteed that the destructor is trivial , Therefore, you must still be careful when using it to declare static variables . Look again https://google.github.io/styleguide/cppguide.html#Static_and_Global_Variables.
otherwise , You'd better use a static variable in a function and return it . see also http://go/cppprimer#static_initialization And the one shown above “ Ordinary function ” Example .

Links for further reading and collection

https://google.github.io/styleguide/cppguide.html#Static_and_Global_Variables
http://en.cppreference.com/w/cpp/language/constexpr
http://en.cppreference.com/w/cpp/language/inline
http://en.cppreference.com/w/cpp/language/storage_duration( Link rule )
http://en.cppreference.com/w/cpp/language/ub( No behavior defined )

Conclusion

C++17 The inline variables in are not fast enough . Until then, , All we can do is use safe idioms , Let's stay away from rough edges .

  1. We come to a conclusion , stay C++17 Language standard [lex.string] in , String literals do not need to be evaluated as identical objects from the following languages . C++11 and C++14 There are also equivalent languages in .

  2. [lex.string] There is no language description in constexpr Different behaviors in context .

原网站

版权声明
本文为[-Flying crane-]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/188/202207071522591220.html