C++ has this funky quirk of supporting initializer lists for a ctor, such as:
class Foo
{
public:
Foo(int x) : m_x(x) { }
private:
SomeComplexObjectThatTakesAnIntForConstruction m_x;
}
Makes sense so far. More efficient because the member is only initialized once, rather than being default-constructed, and then operator= assigned a value later.
But I commonly come across programmers who put the ctor in their .cpp file, where I can hardly believe it actually has the intended (efficient) effect of actually using the initializer list correctly:
// Foo.cpp
Foo::Foo(int x) : m_x(x)
{
// complex set of things needed to be done, or perhaps dependency-inducing references here...
}
开发者_C百科
As I understand things, the above won't necessarily generate a single construction for m_x, because the initializer-list is not visible outside of this translation unit, and will result in construction + assignment, no?
// user.cpp
Foo my_foo(9); // how can the ctor for m_x be effectively inlined here?
Or have I misunderstood how initializer-lists function?
Thanks for your help with this ;)
I have chosen to split the initializer-list and body of the construction into two pieces, such as:
class Foo
{
public:
Foo(int x) : m_x(x) { Initialize(); }
private:
void Initialize(); // defined in our .cpp thus isolating dependencies and creating a common call-point for multiple ctors (if present)
SomeComplexObjectThatTakesAnIntForConstruction m_x;
}
You have misunderstood.
The initializer list doesn't need to be visible from other translation units the same way that the constructor body doesn't need to be visible from other translation units. It affects the code which is generated for the constructor itself, not the code which is generated to call the constructor.
Maybe this will clear up the confusion:
Inlining is one particular optimization. It is not the only type of optimization possible. Modern C++ compilers are capable of performing all sorts of other optimizations (loop unrolling, reordering of statements when they don't affect the program's behavior, etc).
The "short cut" or "efficiency gain" that inlining gives you is the elimination of the need to create a new frame on the call stack. Typically, the code generated for a function call looks something like this, where lines prefixed by --
are part of the called function (assuming the C calling convention).
Push the arguments on to the stack
Push the current code address onto the stack
Jump to the address of the function
-- Move the stack pointer forward to create space for local variables
-- Execute the body of the function
-- Move the stack pointer back to remove the local variables
-- Pop the caller's address from the stack and jump to it
Pop the arguments from the stack
If the function is inlined, this becomes just the first three steps performed by the called function:
-- Move the stack pointer forward to create space for local variables
-- Execute the body of the function
-- Move the stack pointer back to remove the local variables
This optimization relies on the ability of the compiler and/or linker to change where code is generated, not what code is generated.
In contrast, the initializer list affects what code is generated, not where it is generated. The compiler can still generate calls to non-default constructors for member variables whether it is doing it directly at the call site or in a separate section of of the program code that the call will jump to.
Initializer lists work fine when implemented in .cpp files - what makes you believe they wouldn't?
An initializer list is still part of the constructor 'call'. It's just a syntax that formalizes how construction of class members will take place (note for novices - it doesn't direct or influence the order of class member construction, but it allows parameters to be passed to the member's constructors). This makes possible the simple rule that when the first statement after the opening brace is reached, all class members have been through their construction, but it doesn't mean that the initializer list needs to occur before the constructor is called.
To address Mordachai's comment:
Having the init list in the header vs. in the .cpp file would affect the 'inline-ability' of the constructor (or the initialization list, if you're deferring the main work of the constructor to a function call in the inlined ctor). However, that's true of any in-header implementation of a member function vs. an implementation in a .cpp file.
I suspect that for most ctors, performance concerns will be due to resource allocation -if they aren't acquiring a resource, they're probably not going to have perf issues - and that's going to take the same amount of time whether the ctor is inlined or not. Note that this still means that init lists are important, whether they're inlined or not, because they prevent the situation (that you mentioned in your question) where:
- a member object is default initialized (possibly acquiring a resource in an expensive operation)
- re-initializing the member object (which may result in releasing the resource, then acquiring a new resource)
Since resource acquisition/release is typically expensive (whether that resource is memory, a network connection, opening a file) compared to many other things, this is an important anti-pattern to avoid. However, the performance difference between whether these resource acquisitions are inlined or not is probably not significant in most cases, I'd think.
Of course, there are also correctness issues that are addressed by the initialization list. For example, since const
members can't be modified, they must be initialized in an initialization list.
The purpose of the initializer-list is not simply a matter of efficiency.
Aside from the cases where a member must be initialized there because there is no way to do otherwise (references, const-members, class members with no default constructor), it is generally "preferred" in the same way you initialise variables when you first declare them.
There are occasions where it is better to use the constructor body to set variables to their correct values, for example if you have two pointers that will point to objects created with new, and you are scared the second new may throw. In this case you should still "initialize" them - to NULL - then create them in the body, the first one inside an auto_ptr just in case (which you release after the second one works).
The purpose of moving the constructor body into the compilation unit is to hide the implementation detail from the interface. This is generally preferred for maintainability which a lot of time is hugely more important than a minor amount of runtime efficiency that saves microseconds.
I think initializer lists are there not for efficiency, but for semantics.
For one, they are a chance to initialize members before the superclass's constructor gets called, which could call virtual member functions, coming back invisibly into the lower-level class, before the lower-level constructor had finished.
For another, they are a way of guaranteeing that certain fields are initialized to non-garbage values, unlike assignment statements inside the constructor code that swim in a syntax of if-statements, loops, etc, that the compiler can't be sure will get executed.
For another, it lets you declare the class as const while still allowing you to initialize it. (But I'm not positive about than one.)
精彩评论