Letters From Our Readers
The following critical letters pertain to the recently published piece,"Poorly Designed Features of C++ Constructors," Journal of Object-Oriented Programming,
(13)11: 4–10, March 2001. The authors were offered the opportunity to write a rebuttal but declined. I wish to thank the authors of the original piece, as well as the authors of these letters for presenting technical issues related to C++ constructors. I regret the errors in the original piece and hope that these letters clarify the issues.
Dr. Richard Wiener, Editor
I am sorry to say this, but in my humble opinion, the article by Eyas El-Qawasmeh and Basel Mahafzah ("Poorly Designed Features of C++ Constructors," Journal of Object-Oriented Programming, (13)11: 4–10, March 2001) was not very good and certainly not what I would call up to par with the usual standards of the Journal of Object-Oriented Programming. For one thing, I think the authors were not well served by the reviewers and editors at the journal, since there are multiple confusing sentences and what appears to be misstatements of fact that may be only poorly written text. In addition, there appeared to be several actual errors.
Please let me address some of these issues. In the first paragraph on page 4 the authors state, "Constructors may be explicitly called by the programmer code or implicitly called by the compiler when the programmer does not specify any constructor." I believe that instead of "called," the authors meant to say "provided," "declared," or "defined," since the compiler will implicitly call either a programmer-supplied constructor or generate one automatically. That is to say, whether or not the programmer provides a constructor makes no difference to the compiler's implicit calling of that constructor.
In the last paragraph on page 4, the authors state, "The copy constructor, often called X (X&) ..." and in the continuation of that paragraph on page 5 they state, "It (the compiler) uses the constant form if it can; otherwise the non-const form is used." Later, the authors note that copying an object should never modify the object being copied. The copy constructor should always be declared as X(const X&). There will never be any reason why either the compiler or programmer would ever need a copy constructor that takes a non-const reference, because either a const or non-const object reference can be provided where a const reference is expected. Always providing a copy constructor in the canonical form (X(const X&)) will also prevent anyone who works with the implementation of that copy constructor from accidentally violating the const rule of the copied object.
Further down the first column on page 5, in the same section on the copy constructor, the authors state, "In fact, the copy constructor is implemented using the member functions that implement the default constructor and the assignment operator."
While this is certainly one possible way to implement a copy constructor, it is definitely not the best way because it initializes the object twice rather than once. There is no need to default initialize an object only to change its state to the object being copied. Simply initialize the object as a copy in the first place and be done with it. While some programmers may use the two-step process as the authors describe, it is certainly not what the compiler provides as a default copy constructor. The compiler-provided default copy constructor does member-wise initialization from the source object to the new object. The authors' statement near the top of the second column on page 5 that says "the compiler automatically defines an implicit one that performs a bitwise copy from the existing object" is in direct disagreement with Section 12.8, paragraph 8 of the ISO/ANSI C++ Standard.1
In the first paragraph of the section on page 6 entitled "Constructors Can be Inlined, But Should be Prohibited," the authors' explanation of how to inline the constructor presents only one approach. An common alternative approach that simply defines the constructor at the point of declaration is shown below:
class X
{
public:
X(int )
{
...
}
};
Later in this same section, also on page 6, second column, the authors speak of "defining an inline static constructor" and state that "the static constructor is really an object in the guise of a function." I cannot tell what the authors are describing here since there is no such thing as a "static constructor."
Section 12.1, paragraph 4 of the C++ Standard explicitly states: "A constructor shall not be virtual or static."1 Based on the discussion in this section, I can only presume that the authors are talking about a static object rather than a static constructor. And yes, a static object declared and defined in a header file will be instantiated in each object module that includes that header. It is exactly this fact that leads us to the elegant "Schwarz solution," a widely demonstrated first in Jerry Schwarz's early implementation of the C++ I/O library.2
The authors later note on page 7 that "Constructors cannot be declared static" and also note that "some compilers do not implement the initializer reliably." For a compiler to not get its calling of static initializers correct is a serious bug that makes the compiler useless for any real work. It is egregious to use a compiler bug as a language complaint.
In the section "Constructors Do Not Have a Return Type," the authors make several points about how it is not possible to tell if the memory allocation for an object has succeeded or not, and state, "There is no simple way to allocate the memory occupied by an object from within the constructor for that object."
The authors continue with, "You will find that throwing an exception in a constructor may result in the object remaining allocated." This simply reflects a poor understanding of how constructors work. There is never any need for a constructor to worry whether or not the memory for the object being constructed has been allocated. The memory allocation, whether on the stack or the heap, must have succeeded—otherwise the construction would not have been called. The compiler must have what it believes to be a valid address of storage for the object being constructed or it would have nothing with which to initialize the object's "this" pointer. Thus, the mere fact that a constructor is called indicates that the compiler believes it has a valid "this" pointer for the object being constructed.
In this same section, the authors describe the new-handler function and refer to it as a "built-in exception handler." While the new-handler may be used as the authors describe, it should not be referred to as an exception handler because that is a specific term referring to a catch block. In addition, the default behavior on an allocation failure is defined to be the throwing of a std::bad_alloc exception. With all due respect, providing this out-of-date information does your readers a disservice.
On page 10, the authors again write, "Bitwise copies are unacceptable for classes that use dynamically allocated memory" and repeat their earlier statement, "This generated copy constructor simply performs a bitwise copy." As noted earlier, this is not standard conformant behavior.
Finally, in the section "The Compiler Can Select a Coercion Constructor Implicitly" the authors state "If it is essential to retain full control, do not define any constructors that take a single argument." As noted earlier in the article, C++ now provides the keyword "explicit" to control this behavior and, thus, your proposed solution is unnecessary.
I appreciate that the authors must have put a fair amount of work into this article, but my advice would be to get a copy of the ISO/ANSI C++ Standard (available for download from www.ansi.org) and upgrade your compiler to a more conforming version. Much of the article is based on problems and issues that are no longer relevant, and the authors could save themselves a lot of trouble by getting more up-to-date tools. As noted, the reviewers and editors should have helped the authors to produce a better article. I apologize if anyone has been in any way offended by these remarks. They are offered solely in the spirit of open communication and feedback that is required for each of us as individuals, and the C++ community as a whole, to continue to improve.
Randy Maddox
[email protected]
References
- ISO/IEC 14882: Information Technology, Programming Languages—C++, ISO/IEC, 1998.
- Stroustrup, Bjarne. The Design and Evolution of C++, Addison–Wesley, Reading, MA, 1994.
I read the article by Eyas El-Qawasmeh and Basel Mahafzah ("Poorly Designed Features of C++ Constructors," Journal of Object-Oriented Programming, (13)11: 4–10, March 2001) on the magazine's Web site (http://www.adtmag.com). The critical title and the fact that it was published in the Journal of Object-Oriented Programming should require a solidly founded article. It is a highly serious matter to claim that the constructors in the C++ programming language have poorly designed features and to suggest that future modification to C++ can avoid these features. The article only reveals that the authors have inadequate knowledge of the subject and the editors of the journal allowed a very flawed article to make it into print. The content of the article ranks from the harmless, to providing bad advice, to downright errors and misconceptions, and confuses problems in the implementation of some compilers with problems in the C++ language.
The authors write, "Constructors appear to have some poorly designed features for many reasons, such as the condition, which requires the class constructor's name to be the same as the class name." This is a harmless suggestion that does not provide any improved functionality. It doesn't solve any problem or ambiguities—it is only a syntactic thing. It is much like suggesting that it is better to write "begin" instead of "{". The naming of constructors has been adopted by Java and C#. It is probably a good idea to leave it as it is.
The authors state, "Among these tasks is to allocate memory for an object on the stack (static allocation), and on the heap (dynamic allocation)." This is downright wrong. The constructors do not in any way participate in the allocation of the object's memory. There is a very important distinction between allocation and construction in C++. The constructor is used to turn raw memory into a valid object.
Allocation is handled by operator new (in some of its forms) or by the compiler for objects on the stack and in the global memory. It is wrong to call objects on the stack static "allocated"—it might be called automatic allocated. The global object might be called static allocated. (See 3.7.1, 3.7.2 and 3.7.3)2
The authors continue, "Constructors have no return type even though they may have return statements." This statement is at best without any useful information. The same is true for void functions. It is more related to the return statement than to the constructor. The authors state, "It has a single argument that is a reference to an object of a class that will not change (since it is const)." This is not a requirement of copy constructors. As stated in the ISO C++ Standard, "A copy constructor for a class X is a constructor with a first parameter of type X& or of type const X&."2
The authors go on to say, "If a copy constructor is not used in the first two cases, it will lead to an object pointer pointing to deleted memory." This sentence does not make sense to me. I can't figure out what the intention was. In fact, a copy constructor is not required to be called. (See 12.2.)2 This is called return value optimization.
Next the authors claim, "This requires the use of a copy constructor in order to allocate memory." This [statement] again confuses allocation and construction. Allocation is not the responsibility of the constructor.
The authors go on, "In fact, the copy constructor is implemented using the member functions that implement the default constructor and the assignment operator." This is downright wrong. It would require the presence of the default constructor to be able to create a copy constructor. The statement also confuses construction and assignment. In fact, Andrew Koenig wrote an article entitled "Using Constructors For Assignment" on this subject in the February 1995 issue of C++ Report.3 Koenig's article explains the details of why it is a bad idea.
The authors continue, "If there is no copy constructor explicitly provided in the class definition, the compiler automatically defines an implicit one that performs a bitwise copy from the existing object to the new object. The implicit copy constructor simply chains to the copy constructor of each class member." These two statements contradict each other. It is a common misconception that the generated copy constructor performs a bitwise copy, while in fact, it calls the copy constructor for base classes and members. This can, for some types, be implemented as bitwise copy. That does not make it a conceptual bitwise copy.
"Copy constructor makes the programs more efficient because it does not need to change the input argument in order to initialize the new object. It is a good style to always define a copy constructor, even the default copy constructor from the compiler allocating the memory you wanted." This statement does not make any sense to me. There is again confusion about allocation and initialization.
The authors discuss additions to C++ as follows: "Note that recent additions to the C++ standard added the keyword explicit." This sentence lacks precision. The keyword explicit is not an addition to the C++ Standard. It has always been part of the C++ Standard as published.2 It has not always been part of the C++ programming language as described.3 It has been present for years. It is not a problem of the C++ programming language that some compilers do not support this feature—it is a problem of the compiler. Even Microsoft Visual C++ V6, released in 1998 before the standard, supports the feature.
The authors further state, "Among these functions is the constructor, which can also be inlined, but should not be." I feel this is very bad advice. The article does not provide any convincing argument as to why the inlining of constructors is a bad idea.
The authors then claim, "This is efficient for functions that are very short—one or two statements." It is easy to find situations where longer, inlined constructors improve performance. It sometimes allows the optimizer in the compiler to do some optimization, which would otherwise be very hard to do. It can transform runtime calculations into compile time calculations. I have done that to solve real-world problems—implementing a fix-point number class. If the object was constructed with a constant, all calculations required for constructing the object were done at compile time instead of at runtime. That's an example of ultimate optimization.
The authors continue with their argument; "An example showing the dangers of using inline can be seen by defining an inline static constructor. In this case, the static constructor is supposed to be called by the runtime only once. However, if a header file contains an inline static constructor in multiple translation units, then there will be multiple copies." There is no such thing in C++ as a static constructor—which the authors themselves state later in the article. There are static objects. The semantics of static objects (or the semantics of the constructor) are not influenced by whether the constructor is inlined or not. There can be specific problems with bugs in specific compilers, but that is not an issue of the C++ language.
The authors then begin to draw their conclusion: "As a result, there is no standardization in complying with using inline request." This is incorrect. The standard is clear about this issue, so a compiler can comply with the standard. (See 7.1.2.)2
The authors state that, "Throwing an exception in a constructor is tricky." Correct error handling is tricky in general. Throwing an exception in a constructor is a simple and consistent way to handle it. (See Appendix E, also available from Bjarne Stroustrup's home page at http://www.research.att.com/~bs/).3
"There is no simple way to allocate the memory occupied by the object from within the constructor for that object." This does not make any sense to me. It seems to be the same confusion between allocation and construction again. Exceptions and allocation work very well together in C++.
The authors further state, "You will find that throwing an exception in a constructor may result in the object remaining allocated." No, not easily, and only if the constructor allocates resources, which it doesn't release before throwing an exception. But of course, you can always make a resource leak (and any other error) in C++ (and any other programming language). It is well documented how to handle exceptions in constructors in order to write correct, exception safe code. This should be encouraged.
The authors then say, "If there are operations performed in the constructor that can fail, it might be a better idea to put those operations into a separate initialization function, rather than throwing an exception in the constructor." This is very bad advice, as described in Stroustrup.1
The authors' report that "We have found that some compilers do not implement the initializer function reliably." This is not an issue of C++ and constructors. It is an issue of a specific bug in a compiler.
The authors then state, "In the following example, the selection is based on the standard library's type_info construct, by enabling the runtime type information support in some compilers like Microsoft Visual Studio, but you could implement your own Run-Time Type Information (RTTI) scheme based on virtual member functions." However, RTTI is part of ISO C++—it is not an option. The extensions provided by some compilers is not relevant to a discussion of "Poorly Designed Features of C++ Constructors."
The source code: typeid(*this).raw_name() is not C++. The code: raw_name is not a member of std::type_info., but rather name is a member of std::type_info. This is a redundant language extension provided by a specific compiler (Microsoft Visual C++).
The authors then address inheritance. "The need for a default constructor occurs when inheritance is used." This is almost unbelievable nonsense. It is a basic mistake and fundamentally wrong!
The authors continue, "To be more specific, when the most-derived class of a hierarchy is a constructor ..."—how can the most derived class of a hierarchy be a constructor? I guess the authors are talking about virtual base classes. When we are dealing with virtual base classes, the most derived class has the responsibility for constructing the virtual base classes. But this does not require a default constructor—not at all. The authors' example can be rewritten as follows:
class Base
{
int x;
public :
Base(int a) : x(a) { }
};
class alpha : virtual public Base
{
int y;
public :
alpha(int a) : Base(a), y(2) { }
};
class beta : virtual public Base
{
int z;
public :
beta(int a) : Base(a), z(3) { }
};
class gamma : public alpha, public beta
{
int w;
public :
gamma ( int a, int b) : Base(a+b), alpha(a),
beta(b), w(4) { }
};
int main()
{
alpha a(1);
beta b(2);
gamma g(3,4);
return 0;
}
Note the absence of the default constructor. It has been that way for many, many years. It compiles without any problem with Microsoft Visual C++ V6.
The section "Bitwise copies are unacceptable for classes that use dynamically allocated memory," could have been named "Default copy construction is unacceptable for classes that use dynamically allocated memory." It does not provide any new information, nor does it suggest any way to improve the situation. Programmers using C++ need to know when and how to write a copy constructor—and when not to. In the section "The compiler can select a coercion constructor implicitly," the authors are really teaching a bad form of C++. Why not use explicit constructors? They are made for just that purpose, and the authors even mention the explicit keyword earlier in the article. The code should be something like:
class Money
{
public:
Money();
// Define conversion functions that can only be
// called explicitly.
explicit Money( char * ch );
explicit Money( double d );
private:
double _amount;
};
The authors continue with, "It should be clear that the mentioned points are applicable to ANSI C++ to the best of our knowledge." It is clear that the points are not applicable to ANSI C++, and that the authors' knowledge needs to be improved significantly if their comments are to be taken seriously. The authors then claim, "It was observed that many modern compilers handle these points incorrectly." That is very far from my experience during the last 10 years using C++.
The authors continue, "The purpose of the explored points is to take precautions during the compiler construction." I think that it is more appropriate the Journal of Object-Oriented Programming publish articles for ordinary users of C++, rather than for C++ compiler implementers. Ordinary users probably know all the details of constructor semantics in C++.
The level of quality in the discussed article is unacceptable to me. What the C++ community needs is better education, not misinformation. I expect the Journal of Object-Oriented Programming to provide some of that qualified education. Some general advice is that writers do their homework very carefully before criticizing C++. After all, it is the work of highly capable people with a lot of insight and experience. It is not that C++ is perfect—you just need to know what you are talking about.
Mogens Hansen
References
- Stroustrup, Bjarne. The C++ Programming Language, Special Edition, Addison–Wesley, Reading, MA, 2000.
- ISO/IEC 14882: Information Technology, Programming Languages—C++, ISO/IEC, 1998.
- Stroustrup, Bjarne. The C++ Programming Language, Addison–Wesley, Reading, MA. 1986.
- Koenig, Andrew. "Using Constructors For Assignment," C++ Report, Feb. 1995.
I am writing to address several technical inaccuracies in the article by Eyas El-Qawasmeh and Basel Mahafzah ("Poorly Designed Features of C++ Constructors," Journal of Object-Oriented Programming, (13)11: 4–10, March 2001). This article repeatedly confuses allocation with initialization. The two are disjoint operations (and may be split up as such by the user). This is illustrated in the statement T * p = new T. It is equivalent to:
T * p = (T *) operator new(sizeof(T));
// allocate memory
try {
new (p) T; // call constructor
} catch (...) {
operator delete(p); // deallocate memory
throw; // propagate exception
}
The statement
delete p; is equivalent to:
p->~T(); // call destructor
operator delete(p); // deallocate memory
This article also repeatedly states that the default copy constructor performs a bitwise copy. This is not true; the default copy constructor performs a memberwise copy.
In the introduction of the article, the authors state, "Defining an explicit constructor is highly recommended, since the implicit defined constructor may not behave as desired, especially if dynamic allocation of memory is involved." Actually, implicitly defined constructors have well defined, sensible default behavior. You should only explicitly define a constructor if you need to change this behavior. Dynamic allocation of memory has no effect on the behavior whatsoever.
In paragraph 4 of the section entitled "Copy constructor," the authors write "The copy constructor should not modify the object that is used to copy." This is true, but only in general. For a common exception, see std::auto_ptr; other resource-based classes would use the same idiom.
The entire third paragraph of the section "Constructors can be inlined, but should be prohibited," is wrong. Static objects are only initialized once, even if they exist as a local static object inside an inlined function, included and called from several different translation units.
The conclusion to the section entitled "Constructors can be inlined, but should be prohibited," is incorrect; using the same reasoning, inlining should never be allowed for any function, due to the compiler adding temporary variables, calls to conversion functions, etc. The basic rule for inlining is be sure you know what your code is doing—everything you tell it to do, and everything the compiler adds for you. This should be applied to all functions, not just constructors.
In the second paragraph of "Constructors do not have a return type," the authors state, "You will find that throwing an exception in a constructor may result in the object remaining allocated." This is entirely false. Throwing an exception from a constructor will always result in the object's memory being released (assuming you use a non-placement form of new).
The conclusion of "Constructors do not have a return type" is also incorrect. Since the memory deallocation is not an issue, exceptions are a perfectly fine way of reporting errors from constructors. Furthermore, this section confuses the new handler with exceptions thrown from constructors; if you throw an exception from a constructor, the new handler is not invoked. It is only invoked if dynamic memory allocation fails.
In the section "Constructors cannot be declared static," to do what the authors are trying to do, simply write a normal class and declare a static instance of it. Static constructors don't make sense—when would it be called? The proper solution is to define a class and have a single static instance of it.
"Constructors cannot be virtual"—it is impossible for constructors to be virtual. Think for a minute: the term "virtual" means to do something that is dependent on the type of the object. But if that action is to construct it, we do not know what type it is. So a "virtual constructor" would have to determine its type based on its type. What the author is actually trying to do is to determine the object type based on external data, and the proper solution is to use a sort of factory class.
There are several mistakes in the section "The need for a default constructor even when it is not called." The primary one is that the default constructor is called (implicitly). This only goes to show that you have to pay attention to what your code is doing; the compiler cannot guess how you want to initialize your multiple virtual base classes, you have to tell it!
"A constructor's address cannot be taken." This is not a drawback because any constructor can be called through placement new. If the authors wish to be able to manipulate compile time ideas (such as class hierarchies) at run time, then they must write the code to do it (factory pattern, etc.).
Paragraph 2 of the section "Bitwise copies are unacceptable for classes that use dynamically allocated memory" is true; memberwise copies are not suitable for classes containing dynamically allocated raw pointers. That is why std::auto_ptr and boost::shared_ptr (see www.boost.org) exist. So, by replacing the raw pointer with the appropriate smart pointer, the default copy constructor becomes adequate and you do not have to write your own copy constructor (this change also usually helps with error handling and cleanup in the constructor and other member functions).
"The compiler can select a coercion constructor implicitly ..."; If you don't want the compiler to do this, just prefix that constructor with the the keyword explicit. I hope these comments clear up some of the confusion generated by this article.
Steve Cleary
[email protected]