Presentation is loading. Please wait.

Presentation is loading. Please wait.

1 Becoming More Effective with C++ … Day Two Stanley B. Lippman

Similar presentations


Presentation on theme: "1 Becoming More Effective with C++ … Day Two Stanley B. Lippman"— Presentation transcript:

1 1 Becoming More Effective with C++ … Day Two Stanley B. Lippman stan.lippman@gmail.com

2 2 The Problem of Static Initialization

3 3 Complex Global Objects Global objects are problematic for many reasons. Their primary benefit is that they simplify information sharing across functions, modules, and libraries. The three primary drawbacks of global objects are The direct use of global objects across functions, modules, and/or libraries results in code that is difficult to reuse or modify in any significant way. In a threaded environment, global objects require locks before write access. Complex global objects are considerably more expensive to initialize, and are difficult to program correctly. In this unit, we look at this last issue, that of complex global objects.

4 4 C-Style strings vs. string Class First, we need to understand what it means to say that a global object is complex. Consider the following pairs of global declarations. In this pair, the first instance is a C-style string. The second is a standard library string object. Both hold a constant string literal indicating a version number. What are the differences in initialization? const char* const version1 = “0.00 ”; const string version2( “0.00 ” );

5 5 C-Style strings vs. string Class The first instance represents a constant expression. version1 can be completely evaluated at compile-time. Typically, the string literal is allocated in the program data segment. (Alternatively, the implementation may use a string table to avoid multiple instances of the same string literal.) version1 is initialized to the location of the string literal during compilation. There is no run-time overhead. const char* const version1 = “0.00 ”;

6 6 C-Style strings vs. string Class The second instance represents a constructor invocation. This in general cannot be evaluated at compile-time. The string literal is still allocated during compilation. The actual invocation of the string constructor, however, must be delayed until program start-up. In addition, if version2 is needed by another global object not defined in the same file, we have a potentially serious dependency problem. (More on that later.) const string version2( “0.00 ” );

7 7 Built-in Array vs. vector Class In this pair, the first instance is a built-in array. The second is a standard library vector object. Both are initialized to the same four constant expressions. What are the differences in initialization? int lut1[] = { 7, 12, 48, 106 }; vector lut2( lut1, lut1+4 );

8 8 Built-in Array vs. vector Class In this pair, the first instance is a built-in array object. The second is a standard library vector object. Both are initialized to the same values. Again, the first instance represents a constant expression. lut1 can be completely evaluated at compile-time. Typically, the array memory is allocated in the program data segment with the elements initialized to the literal values. There is no run-time overhead. (Of course, only constant expressions can be used within the initialization list.) int lut1[] = { 7, 12, 48, 106 };

9 9 Built-in Array vs. vector Class The second instance again represents a run-time constructor invocation. The begin and end address marking off the range of elements with which to initialize lut2 are constant expressions. The iteration across that range to copy the values is a run-time activity. Again, if lut2 is needed by another global object not defined in the same file, we have the same potentially serious dependency problem. vector lut2( lut1, lut1+4 );

10 10 Pointer Initializations In this pair, both are pointers to objects of type int. Both address an integer object containing the value 7. One is initialized to the address of the first element of lut1. The other is initialized with a copy of that element’s value. The object it addresses is allocated on the heap. What are the differences in initialization? int lut1[] = { 7, 12, 48, 106 }; int *pi1 = &lut1[ 0 ]; int *pi2 = new int( lut1[ 0 ] );

11 11 Pointer Initializations In this pair, the first instance is initialized with the address of the first array element. The address of an object is a constant expression: the compiler knows its value. The memory for the pointer is allocated within the data segment by the compiler and initialized with the address of the first element during code generation. There is no run-time overhead. int lut1[] = { 7, 12, 48, 106 }; int *pi1 = &lut1[ 0 ];

12 12 Pointer Initializations In this pair, the second instance invokes the new expression. The new expression is actually a library call. It cannot be evaluated at compile-time. Heap memory, in general, is a run-time resource that requires one or two function calls. So, although this does not involve a class constructor, it still requires a run-time function invocation. int lut1[] = { 7, 12, 48, 106 }; int *pi2 = new int( lut1[ 0 ] );

13 13 Simple Type Initialization In this pair, both objects are the built-in integer data type. What are the differences in initialization? Or, rather, at this point, why is the second instance a run-time initialization? int ival1 = 7; int ival2 = lut1[ 0 ];

14 14 Simple Type Initialization The first instance is initialized to a constant literal. The compiler is able to completely evaluate the expression and complete the initialization during code-generation. The second instance is initialized to the value of a non-constant integer object. The value associated with a non-const object of any type cannot be known until run-time. While no constructor or function needs to be invoked in the initialization of ival2, it still cannot be initialized at compile-time and requires static initialization. int ival1 = 7; int ival2 = lut1[ 0 ];

15 15 Summy: Constant Expressions Each pair’s first definition makes use of constant expressions, and result in compile-time initialization that is loaded within the program’s data segment: const char* const version1 = “0.00 ”; int lut1[] = { 7, 12, 48, 106 }; int *pi1 = &lut1[ 0 ]; int ival1 = 7; A constant expression is an expression that can be fully evaluated at compile-time. Yes, the address of an object is a constant expression! In the C language, global data can only be initialized with constant expressions …

16 16 Summy: Constant Expressions Each pair’s second definition requires run-time evaluation of its initial value. Evaluation must be delayed until program start-up. const string version2(“0.00 ” ); lut lut2( 7, 12, 48, 106 ); int *pi2 = new int( lut1[ 0 ] ); int ival2 = lut1[ 0 ]; The next question, then, is how does this initialization get accomplished during run-time.

17 17 OK, given the following program fragment Matrix identity; int main(){ // identity must be initialized by this point! // identity must be initialized by this point! Matrix m1 = identity; Matrix m1 = identity;...... return 0; return 0;} the language guarantees that identity is constructed prior to the first user statement of main(), and that it is destructed following the last statement of main(). A global object such as identity with an associated constructor and destructor is said to require both static initialization and deallocation.

18 18 Static Initialization: Why the Urgency? OK, given the following program fragment, why is the issue of when static initialization takes place an issue? Matrix identity( 1,0,0,0,0,1,0,0, 0,0,1,0,0,0,0,1 ); 0,0,1,0,0,0,0,1 ); int main() { Matrix m1 = identity; Matrix m1 = identity; //... //... return 0; return 0;}

19 19 Static Initialization: Why the Urgency? Unless identity is initialized before the first statement of main(), our program fails. In this case, it is the compiler’s responsibility to carry out the necessary initialization in a timely fashion. (We’ll see a case later in which the initialization becomes our responsibility.) The language guarantees just that. We are promised that identity is constructed prior to the first user statement of main(), and that it is destructed following the last statement of main(). A global object such as identity with an associated constructor and destructor is said to require both static initialization and deallocation.

20 20 Static Initialization at Start-up All global objects, whether complex or simple, are allocated within the program data segment. If an initial value is specified, and that initialize value can be evaluated at compile-time, the object is initialized with that value. Otherwise, a special initialization function (and special deallocation function, if necessary) is synthesized within the module under compilation. The non-constant initial expression is applied to the object within this function.

21 21 Static Initialization at Start-up For example, in our example, the program is rewritten something like the following (this is Pseudo-code): Matrix identity; // no constructor applied Matrix identity; // no constructor applied int main() int main() { // prior to execution beginning here, // prior to execution beginning here, // __sti__someName() and all other static // __sti__someName() and all other static // initialization functions must be invoked // initialization functions must be invoked Matrix m1 = identity; Matrix m1 = identity; // … // …} // compiler synthesize initialization function // compiler synthesize initialization function void __sti_someName(){ void __sti_someName(){identity.Matrix::Matrix( 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1 ); 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1 ); }

22 22 Static Initialization at Start-up If there are multiple objects within a file requiring static initialization, they are placed within the function in the order of their declaration. This is guaranteed by the language. (What is not guaranteed is the order that these initialization functions are invoked. We’ll look at that in a minute. So, for example, given the following set of complex global objects: const string version2( “0.00 ” ); vector lut2( lut1, lut1+4 ); int *pi2 = new int( lut1[ 0 ] ); int ival2 = lut1[ 0 ];

23 23 Static Initialization at Start-up // typical transformation const string version2; // no constructor vector lut2; // no constructor int *pi2 = 0; int ival2 = 0; void __sti_someName() void __sti_someName() { // Pseudo-code // Pseudo-code version2.string::string(“0.00 ” ); version2.string::string(“0.00 ” ); lut2.vector::vector( lut1, lut1+4 ); lut2.vector::vector( lut1, lut1+4 ); pi2 = new int( lut1[ 0 ] ); pi2 = new int( lut1[ 0 ] ); ival2 = lut1[ 0 ]; ival2 = lut1[ 0 ];}

24 24 Static Initialization at Start-up The job of the compilation system, then, is to identify and invoke all the static initialization functions within the program executable. Moreover, to do that prior to the beginning of main(). Again, within a module, the order of initialization is strictly defined: it is the textual order of declaration. The order of destruction is the reverse. Unfortunately, the order of initialization across modules is left undefined by C++ Standard. Using objects requiring static initialization across modules requires some special programming. Let’s first look at the problem, then illustrate one programmer solution.

25 25 Problem Illustration #include #include #include "my_static_object.h" extern my_static_object *p; // foo makes use of p // foo and p are defined in separate modules extern int foo(); // we want to use p and foo() here – // we have no way within the language // language to force the initialization of p. int iv = p->bar(); int iv2 = foo(); int main() { // !! core dumps before reaching here. // !! core dumps before reaching here. cout << "beginning main: " << p << endl; cout << "beginning main: " << p << endl;}

26 26 The Problem Statement We need to guarantee that when a user includes a header file in which one or more global objects are declared that require static initialization, that those objects Are initialized in that module in which the header file is included. But … are initialized just once although it is likely that multiple modules will include the header file. As an example of the problem, think of cout, cin, and cerr, each of which must be statically initialized before a use and which are included in hundreds of files.

27 27 A Schwarz Counter Solution Jerry Schwarz, the designer of the original iostream library, came up with a solution that is now generally bears his name: Schwarz Counter. Consider our static_object class, as follows: class my_static_object { public:my_static_object(); int foo() const { return _ival; } private: int _ival; }; // the external object we need to initialize extern my_static_object *p;

28 28 A Schwarz Counter Solution Within the header file of the class an auxiliary class – some call it a helper class – and a static object of that class are introduced: class my_stat_obj_init { public: my_stat_obj_init( ); ~my_stat_obj_init();private: static int init_count; static int init_val; }; // the Schwarz counter static object Static my_stat_obj_init obj;

29 29 What’s Going on? The auxiliary class maintains a static data member that keeps a reference count. This member is incremented with each constructor call and decremented with each destructor call. If it is 0 prior to being incremented, the global object we care about is initialized within the constructor: my_stat_obj_init::my_stat_obj_init(){ if ( init_count++ ) return; return; // this is the whole point of the class // this is the whole point of the class p = new my_static_object( init_val ); p = new my_static_object( init_val );}

30 30 Destructor & Static Members The cost of the static object initialization of obj is the constructor call, and generally a word of storage for each module it is included in. The benefit is that it does not require any explicit work from the user. The destructor and static data declarations look as follows: my_stat_obj_init::~my_stat_obj_init(){ if ( -- init_count ) return; return; delete p; }; int my_stat_obj_init::init_count; int my_stat_obj_init::init_val = 1024;

31 31 Potential Drawback … If possible, I recommend not using global objects, in particular global objects requiring static initialization. A singleton object that allocates itself on a first use is one possible alternative. It is important for the C++ programmer to Recognize the difference between simple and global complex object. Recognize when a global complex object may be accessed by complex global objects in other modules and program such that the global object accessed is initialized. Hopefully this unit has clarified the issue and suggested at least one solution.


Download ppt "1 Becoming More Effective with C++ … Day Two Stanley B. Lippman"

Similar presentations


Ads by Google