tag 标签: constructor

相关博文
  • 热度 16
    2012-3-11 11:50
    1584 次阅读|
    0 个评论
    Off and on for about two years, I've been writing about techniques for representing and manipulating memory-mapped devices in C and C++. My more recent columns have been more about C++ than C, focusing on language features such as constructors and new-expressions, which C++ doesn't share with C. 1, 2 Some readers have suggested that the C code I presented is preferable to the C++ code because the C structure implementations for devices are generally simpler than their corresponding C++ class implementations. Last month, I argued that the C++ implementations are actually better because they're easier to use correctly and harder to use incorrectly. Other readers have claimed that my C++ implementations are flawed because they're too simple—they don't use inheritance and preclude the use of virtual functions. Classes with virtual functions can be very useful, but they aren't the solution to every problem. Classes that represent memory-mapped devices, such as the ones I've presented, work with real hardware specifically because they don't use virtual functions. The new C++ Standard acknowledges the usefulness of such classes by defining categories such as standard layout classes , which avoid features such as virtual functions. In the coming months, I'll explain what virtual functions are. I'll show why they can be useful in some applications, but undesirable in classes that represent memory-mapped device registers. I'll also show you how C++ typically implements virtual functions by showing how you can emulate them in C. I'll begin this month by looking at the sort of problem that virtual functions are good at solving. I'll show a typical C solution using a construct called a discriminated union, and examine its limitations. An illustrative problem Suppose you have an application that employs two-dimensional geometric shapes, such as circles, rectangles, and triangles. At a minimum, each shape object contains some linear or angular distances sufficient to characterize the physical extent of the shape. For example, a circle has a radius, a rectangle has a height and a width, and a triangle has two sides and an angle. The shapes may have common attributes as well, such a position (planar coordinates), or outline and fill colors. A fairly traditional C implementation for a shape is a structure with a nested union, such as: typedef struct shape shape; struct shape { coordinates position; color outline, fill; shape_kind kind; union { circle_part circle; rectangle_part rectangle; triangle_part triangle; } u; }; The union member, u, is large enough to hold the largest of its members, but it can only store the value of one member at a time. In this example, each union member has a different structure type. For a circle, you need to store only the radius, as in: typedef struct circle_part circle_part; struct circle_part { double radius; }; For a rectangle, you need the height and width: typedef struct rectangle_part rectangle_part; struct rectangle_part { double height, width; }; For a triangle, you need two sides and an adjacent angle: typedef struct triangle_part triangle_part; struct triangle_part { double side1, side2, angle; }; When the union members have such simple types, you might find it easier to dispense with the named structure types and simply define unnamed structures inside the union, as in: union { struct { double radius; } circle; struct { double height, width; } rectangle; struct { double side1, side2, angle; } triangle; } u; The value of the shape's kind member indicates which union member is currently in use. The shape_kind type enumerates the possible values: enum shape_kind { sk_circle, sk_rectangle, sk_triangle }; typedef enum shape_kind shape_kind; For example, setting a shape's kind member to sk_rectangle indicates that it's now OK to access (write to or read from) the union's rectangle member and its height and width members.    
  • 热度 10
    2011-9-20 00:53
    1984 次阅读|
    0 个评论
      Compilers apply the usual rules for argument matching in overload resolution to find an allocation function that will accept this assembled argument list. 5 The new-expression won't compile if no operator new is visible that will accept the given arguments.   Placement new Bjarne Stroustrup, the designer of C++, explained that his primary motivation for extending the syntax of new-expressions was to pass information about where to place the created object. 6 Hence, he dubbed the argument list after the keyword new the placement syntax . A new-expression that includes the placement syntax is called new with placement or just placement new .   The Standard C++ Library provides a placement form of operator new declared in the standard header as:   void *operator new(std::size_t, void *p) throw ();   Most C++ implementations define it as an inline function:   inline void *operator new(std::size_t, void *p) throw () { return p; }   It does nothing but return the value of its second parameter. It completely ignores its first parameter. The exception-specification throw () indicates that the function isn't allowed to propagate any exceptions.   Programs can use this operator new to construct an object at a particular address. For example, if you declare the timer object as:   extern timer_type the_timer;   you can apply the constructor via the placement new-expression:   new (the_timer) timer_type;   This placement new-expression compiles into a call to placement operator new , followed immediately by a call to the constructor:   operator new(sizeof(timer_type), the_timer); (the_timer)-timer_type();   Recall that this operator new does nothing but return its second argument, and it's typically defined as an inline function. Most compilers completely eliminate the first statement during optimization.   The second statement:   (the_timer)-timer_type();   is equivalent to:   the_timer.timer_type();   which applies the timer_type constructor to the_timer . But, again, these notations are just what I'm using to convey the concept of a constructor call. Neither one actually compiles if you try to write it yourself.   If you define the timer as a constant pointer:   timer_type *const the_timer = reinterpret_cast(0xFFFF6000);   then you can apply the constructor via the placement new-expression:   new (the_timer) timer_type;   This placement new-expression differs from the earlier ones only in that it omits the unary (address-of) operator from the placement argument: the_timer instead of the_timer .   The C++ Standard doesn't specify the order of implicit initialization as precisely as some programmers would like. I've heard many embedded developers express a desire for more explicit control over initialization order. Using placement new offers that control.   For example, if you have some master device that must be initialized before some slave device, just write the calls to placement new in that order within some larger initialization function, as in:   void initialize() { new (the_master) master_type; new (the_slave) slave_type; }     Still room for improvement C++ programmers expect classes to use constructors to perform object initialization. Classes for memory-mapped devices should be no different. However, many common declarations for such objects don't invoke constructors implicitly. Placement new offers a way to invoke constructors explicitly, which might be what you want sometimes, but not necessarily always. At other times, implicit initialization might still be preferable. I'll be looking at other more flexible alternatives in the future.     Endnotes: 1. Saks, Dan, "Memory-mapped devices as C++ classes", Embeddeddesignindia.co.in, March 2011. http://forum.embeddeddesignindia.co.in/BLOG_ARTICLE_7022.HTM . 2. Saks, Dan, "Demystifying constructors," Embeddeddesignindia.co.in , April 2011. http://forum.embeddeddesignindia.co.in/BLOG_ARTICLE_7113.HTM . 3. . Saks, Dan, "Issues when constructing memory-mapped objects," Embeddeddesignindia.co.in , August 2011. http://forum.embeddeddesignindia.co.in/BLOG_ARTICLE_8898.HTM 4. Saks, Dan. "Throw and catch," ESD, May, 2007, p. 11. www.eetimes.com/4026038. 5. Saks, Dan. "Function Name Overloading," Embedded Systems Programming , May 1999, p. 17. www.eetimes.com/4026899. 6. Bjarne Stroustrup. The Design and Evolution of C++ . Addison-Wesley, 1994.
  • 热度 10
    2011-9-20 00:45
    3413 次阅读|
    0 个评论
      Compilers apply the usual rules for argument matching in overload resolution to find an allocation function that will accept this assembled argument list. 5 The new-expression won't compile if no operator new is visible that will accept the given arguments.   Placement new Bjarne Stroustrup, the designer of C++, explained that his primary motivation for extending the syntax of new-expressions was to pass information about where to place the created object. 6 Hence, he dubbed the argument list after the keyword new the placement syntax . A new-expression that includes the placement syntax is called new with placement or just placement new .   The Standard C++ Library provides a placement form of operator new declared in the standard header new as:   void *operator new(std::size_t, void *p) throw ();   Most C++ implementations define it as an inline function: inline void *operator new(std::size_t, void *p) throw () { return p; } It does nothing but return the value of its second parameter. It completely ignores its first parameter. The exception-specification throw () indicates that the function isn't allowed to propagate any exceptions.   Programs can use this operator new to construct an object at a particular address. For example, if you declare the timer object as:   extern timer_type the_timer;   you can apply the constructor via the placement new-expression: new (the_timer) timer_type; This placement new-expression compiles into a call to placement operator new , followed immediately by a call to the constructor:   operator new(sizeof(timer_type), the_timer); (the_timer)-timer_type();   Recall that this operator new does nothing but return its second argument, and it's typically defined as an inline function. Most compilers completely eliminate the first statement during optimization.   The second statement: (the_timer)-timer_type(); is equivalent to: the_timer.timer_type(); which applies the timer_type constructor to the_timer . But, again, these notations are just what I'm using to convey the concept of a constructor call. Neither one actually compiles if you try to write it yourself.   If you define the timer as a constant pointer:   timer_type *const the_timer = reinterpret_cast(0xFFFF6000);   then you can apply the constructor via the placement new-expression:   new (the_timer) timer_type;   This placement new-expression differs from the earlier ones only in that it omits the unary (address-of) operator from the placement argument: the_timer instead of the_timer .   The C++ Standard doesn't specify the order of implicit initialization as precisely as some programmers would like. I've heard many embedded developers express a desire for more explicit control over initialization order. Using placement new offers that control.   For example, if you have some master device that must be initialized before some slave device, just write the calls to placement new in that order within some larger initialization function, as in:   void initialize() { new (the_master) master_type; new (the_slave) slave_type; }     Still room for improvement C++ programmers expect classes to use constructors to perform object initialization. Classes for memory-mapped devices should be no different. However, many common declarations for such objects don't invoke constructors implicitly. Placement new offers a way to invoke constructors explicitly, which might be what you want sometimes, but not necessarily always. At other times, implicit initialization might still be preferable. I'll be looking at other more flexible alternatives in the future.     Endnotes: 1. Saks, Dan, "Memory-mapped devices as C++ classes", Eetasia.com, March 2011. http://forum.eetasia.com/BLOG_ARTICLE_7018.HTM . 2. Saks, Dan, "Demystifying constructors," Eetasia.com, April 2011. http://forum.eetasia.com/BLOG_ARTICLE_7112.HTM 3. . Saks, Dan, "The troubles with constructing memory-mapped objects," Eetasia.com, August 2011. http://forum.eetasia.com/BLOG_ARTICLE_8897.HTM 4. Saks, Dan. "Throw and catch," ESD, May, 2007, p. 11. www.eetimes.com/4026038. 5. Saks, Dan. "Function Name Overloading," Embedded Systems Programming , May 1999, p. 17. www.eetimes.com/4026899. 6. Bjarne Stroustrup. The Design and Evolution of C++ . Addison-Wesley, 1994.  
  • 热度 11
    2011-9-7 22:34
    1944 次阅读|
    0 个评论
    Classes are commonly the best tool for modeling memory-mapped devices  In C++. 1 You can use a class to hide messy details, such as device register layouts and bit masks, behind a simpler and more robust interface. A constructor is a special class member function that provides guaranteed initialization for objects of its class type. 2 Using constructors to initialize objects is common practice in C++. C++ programmers should reasonably expect classes for memory-mapped devices to do initialization via constructors, unless there's some compelling reason to do otherwise. In many embedded systems, the appropriate way to initialize a device is to put it into an inactive state. For example, the constructor for a programmable timer might simply make sure that the timer is disabled, as in: class timer_type { public: ~~~ timer_type() { disable(); } ~~~ }; In a previous article, I explained the problem with getting constructors for memory-mapped devices to execute automatically. 3 This month, I'll explore one way to overcome it. Recapping the problem With most C and C++ compilers, you can name a memory-mapped object using a standard extern declaration such as: extern timer_type the_timer; and then use the linker to map the_timer to the desired address. However, this declaration is not a definition, so the compiler doesn't generate code to allocate storage or call a constructor. Some C and C++ compilers provide a nonstandard language extension that lets you position an object at a specified memory address, such as: timer_type the_timer @ 0xFFFF6000; With most compilers that support this sort of declaration, this isn't a definition, so the compiler won't generate a constructor call for this, either. Defining a pointer or reference to a memory-mapped object, such as: timer_type the_timer = *reinterpret_cast(0xFFFF6000); has the same initialization problem as the previous object declarations. This reference definition doesn't define the memory-mapped object itself. Here, too, the compiler won't generate a constructor call applied to the memory-mapped object. If the compiler won't call a constructor implicitly, why not just write an explicit call, say, immediately after the object or reference declaration? That is, if you declare the timer object as: extern timer_type the_timer; why not write an explicit constructor call to go with it, such as: the_timer.timer_type(); // ? Because it won't compile. C++ won't let you call a constructor as if it were any other class member function. In taking on the job of generating constructor calls automatically, C++ denies you the ability to do it yourself. Well, almost. In truth, although you can't call a constructor using the usual member function call syntax, you can call a constructor using a particular form of new-expression known as placement new-expression . To appreciate how it works, let's first review new-expressions in general. New-expressions In C++, you typically allocate dynamic storage using a new-expression such as: pt = new T; where T is a type and pt is an object of type "pointer to T ". When T is a class type, the new-expression not only allocates storage, but also invokes a constructor. Thus, using new is generally preferable to using the standard malloc function. Whereas malloc allocates raw storage of indeterminate value, new can create objects with coherent initial values. A new-expression allocates memory by calling a function named operator new . Each C++ environment provides a default global allocation function declared as: void *operator new(std::size_t n); As with malloc , the argument to operator new is the size (in bytes) of the storage request, and the return value is the address of the allocated storage. However, operator new reports failure differently from malloc . Whereas malloc returns a null pointer if it can't allocate the requested storage, operator new throws an exception. 4 Thus, for a class type T , a new-expression such as: pt = new T; translates more-or-less into something like: pt = static_cast(operator new(sizeof(T))); pt-T(); The first statement acquires storage for a T object by calling operator new , and converts the address of that storage from type void * to type T * . The second initializes the storage by applying T 's default constructor. As I mentioned earlier, a C++ compiler won't let you write this explicit constructor call, but it's happy to do it for you. If class T has a constructor that accepts arguments, you can get the new-expression to call that constructor by providing the constructor arguments as part of the new-expression, as in: pt = new T (x, y, z);. In this case, the new-expression translates more-or-less into something like: pt = static_cast(operator new(sizeof(T))); pt-T(x, y, z); Overloading operator new As with any other function, you can overload operator new by simply declaring additional functions with the same name but different parameter types. For example, in addition to the standard allocation function: void *operator new(std::size_t n); you might declare: void *operator new(std::size_t n, other_info i); where other_info is some user-defined type for conveying additional information to the allocation function. That's simple enough, but how can you get a new-expression to use this alternative allocation function? The problem is finding some way to pass the additional argument to operator new . You can't add a parenthesized argument list after the type name in the new-expression because the compiler will interpret that list as arguments to a constructor, not as arguments to an operator new . That is: pt = new T (x); passes x as an argument to a T constructor, not as an additional argument to operator new . Rather, you must squeeze the additional argument(s) to the allocation function into some other spot in the new-expression. That spot is between the keyword new and the allocated type. For example, the new-expression in: other_info info; ~~~ pt = new (info) T; specifies info as an additional argument to operator new . As always, the new-expression uses sizeof(T) as the first argument to operator new . Thus, the new-expression results in a call to: operator new(sizeof(T) , info ).  
  • 热度 10
    2011-9-7 22:26
    3365 次阅读|
    0 个评论
    In C++, the best tool for modeling memory-mapped devices are usually classes. 1 You can use a class to hide messy details, such as device register layouts and bit masks, behind a simpler and more robust interface. A constructor is a special class member function that provides guaranteed initialization for objects of its class type. 2 Using constructors to initialize objects is common practice in C++. C++ programmers should reasonably expect classes for memory-mapped devices to do initialization via constructors, unless there's some compelling reason to do otherwise. In many embedded systems, the appropriate way to initialize a device is to put it into an inactive state. For example, the constructor for a programmable timer might simply make sure that the timer is disabled, as in: class timer_type { public: ~~~ timer_type() { disable(); } ~~~ }; In a previous article, I explained the problem with getting constructors for memory-mapped devices to execute automatically. 3 This month, I'll explore one way to overcome it. Recapping the problem With most C and C++ compilers, you can name a memory-mapped object using a standard extern declaration such as: extern timer_type the_timer; and then use the linker to map the_timer to the desired address. However, this declaration is not a definition, so the compiler doesn't generate code to allocate storage or call a constructor. Some C and C++ compilers provide a nonstandard language extension that lets you position an object at a specified memory address, such as: timer_type the_timer @ 0xFFFF6000; With most compilers that support this sort of declaration, this isn't a definition, so the compiler won't generate a constructor call for this, either. Defining a pointer or reference to a memory-mapped object, such as: timer_type the_timer = *reinterpret_cast(0xFFFF6000); has the same initialization problem as the previous object declarations. This reference definition doesn't define the memory-mapped object itself. Here, too, the compiler won't generate a constructor call applied to the memory-mapped object. If the compiler won't call a constructor implicitly, why not just write an explicit call, say, immediately after the object or reference declaration? That is, if you declare the timer object as: extern timer_type the_timer; why not write an explicit constructor call to go with it, such as: the_timer.timer_type(); // ? Because it won't compile. C++ won't let you call a constructor as if it were any other class member function. In taking on the job of generating constructor calls automatically, C++ denies you the ability to do it yourself. Well, almost. In truth, although you can't call a constructor using the usual member function call syntax, you can call a constructor using a particular form of new-expression known as placement new-expression . To appreciate how it works, let's first review new-expressions in general. New-expressions In C++, you typically allocate dynamic storage using a new-expression such as: pt = new T; where T is a type and pt is an object of type "pointer to T ". When T is a class type, the new-expression not only allocates storage, but also invokes a constructor. Thus, using new is generally preferable to using the standard malloc function. Whereas malloc allocates raw storage of indeterminate value, new can create objects with coherent initial values. A new-expression allocates memory by calling a function named operator new . Each C++ environment provides a default global allocation function declared as: void *operator new(std::size_t n); As with malloc , the argument to operator new is the size (in bytes) of the storage request, and the return value is the address of the allocated storage. However, operator new reports failure differently from malloc . Whereas malloc returns a null pointer if it can't allocate the requested storage, operator new throws an exception. 4 Thus, for a class type T , a new-expression such as: pt = new T; translates more-or-less into something like: pt = static_cast(operator new(sizeof(T))); pt-T(); The first statement acquires storage for a T object by calling operator new , and converts the address of that storage from type void * to type T * . The second initializes the storage by applying T 's default constructor. As I mentioned earlier, a C++ compiler won't let you write this explicit constructor call, but it's happy to do it for you. If class T has a constructor that accepts arguments, you can get the new-expression to call that constructor by providing the constructor arguments as part of the new-expression, as in: pt = new T (x, y, z);. In this case, the new-expression translates more-or-less into something like: pt = static_cast(operator new(sizeof(T))); pt-T(x, y, z); Overloading operator new As with any other function, you can overload operator new by simply declaring additional functions with the same name but different parameter types. For example, in addition to the standard allocation function: void *operator new(std::size_t n); you might declare: void *operator new(std::size_t n, other_info i); where other_info is some user-defined type for conveying additional information to the allocation function. That's simple enough, but how can you get a new-expression to use this alternative allocation function? The problem is finding some way to pass the additional argument to operator new . You can't add a parenthesized argument list after the type name in the new-expression because the compiler will interpret that list as arguments to a constructor, not as arguments to an operator new . That is: pt = new T (x); passes x as an argument to a T constructor, not as an additional argument to operator new . Rather, you must squeeze the additional argument(s) to the allocation function into some other spot in the new-expression. That spot is between the keyword new and the allocated type. For example, the new-expression in: other_info info; ~~~ pt = new (info) T; specifies info as an additional argument to operator new . As always, the new-expression uses sizeof(T) as the first argument to operator new . Thus, the new-expression results in a call to: operator new(sizeof(T) , info ).