11

[this question has been highly edited; pardon that, I've moved the edits into an answer below]

From Wikipedia (subarticle included) on C++11:

This [new delegating constructors feature] comes with a caveat: C++03 considers an object to be constructed when its constructor finishes executing, but C++11 considers an object constructed once any constructor finishes execution. Since multiple constructors will be allowed to execute, this will mean that each delegating constructor will be executing on a fully constructed object of its own type. Derived class constructors will execute after all delegation in their base classes is complete."

Does this mean that delegation chains construct a unique temporary object for every link in a ctor delegation chain? That kind of overhead just to avoid a simple init function definition would not be worth the additional overhead.

Disclaimer: I asked this question, because I'm a student, but the answers thus far have all been incorrect and demonstrate a lack of research and/or understanding of the research referenced. I've been somewhat frustrated by this, and as a result my edits and comments have been hastily and poorly composed, mostly over smart phone. Please excuse this; I hope I've minimized that in my answer below, and I have learned that I need to be careful, complete, and clear in my comments.

okovko
  • 1,851
  • 14
  • 27
  • 10
    *"Do delegation chains really construct a unique temporary object for every link in a ctor delegation chain?"* I don't quite understand how you infer that from the wikipedia article. – dyp Oct 14 '15 at 04:46
  • I addressed this in my edit of my question, and removed my redundant (and poorly phrased) comments here. Check it out. +1 for pointing out the need for clarity! – okovko Oct 15 '15 at 22:11
  • Am I right to conclude that this comment [_"Clarifying the rest: the implementation may just write on the same stack address (no ctors required, that was a blunder)"_](http://stackoverflow.com/questions/33115803/do-c11-delegated-ctors-perform-worse-than-c03-ctors-calling-init-functions/33161312#comment54127093_33115832) essentially changed the question? It seems to remove the contentious claim that "delegation chains construct a unique temporary object for every link in a ctor delegation chain", if I get you correctly. – sehe Oct 16 '15 at 08:00
  • I've been a smart ass. Honestly, I don't know what any particular implementation does or doesn't do to optimize some cases. What I do know, is that the standard draws an analogue between how member objects are destructed within a destructing object, and how objects created by constructors are destructed within a delegating constructor call. That is to say, the latter objects are treated like member objects. I'm talking about 15.2 from N3242. I quoted it below, it's a little quicker to find that way. – okovko Oct 16 '15 at 11:57

4 Answers4

4

No. They are equivalent. The delegating constructor behaves like an ordinary member function acting on the Object constructed by the previous constructor.

I couldn't find any information explicitly supporting this in the proposal for adding delegating constructors, but creating copies is not possible in the general case. Some classes may not have copy constructors.

In Section 4.3 - Changes to §15, the proposed change to the standard states:

if the non‐delegating constructor for an object has completed execution and a delegating constructor for that object exits with an exception, the object’s destructor will be invoked.

This implies that the delegating constructor works on a completely constructed object (depending on how you define that) and allows the implementation to have delegating ctors work like member functions.

nishantjr
  • 1,788
  • 1
  • 15
  • 39
  • Can you provide a source for that? edit: not that Wikipedia sources its claim, but I'd like to be sure, thank you for answering – okovko Oct 14 '15 at 02:58
  • Can't find a source - perhaps it's implement defined, but there's no reason to implement it that way. I've made some edits that perhaps cover your query – nishantjr Oct 14 '15 at 04:24
  • What? No, it doesn't imply that. It explicitly states the opposite. It states that an object has completed being constructed and its destructor is called. You really think they wouldn't just write that it's treated like a member function if that was the case... And yeah, you might not have a copy ctor, but you don't need one. The stack pointer isn't changed throughout the ctor calls. The objects are default move constructed, overwriting the stack. Probably.. – okovko Oct 14 '15 at 20:50
  • Actually that's probably not true either. The ctors act on an object of their own type, so you can't use the same chunk of stack if there's any inheritance, because the base and derived are probably different sizes. – okovko Oct 14 '15 at 20:56
  • Move constructors can be deleted or unimplemented too. Opposite of what? It seems to be leaving the implementation up to the compilers. "the stack pointer isn't changed" implies that it is the same object - no copies – nishantjr Oct 15 '15 at 05:19
  • You summarize your source and claim it says something contradictory to what you just summarized. Your quote describes B : B', where B is the delegating ctor and B' is the non-delegating ctor. B works on a completely constructed object (defined in N3242 as an object whose principal ctor [first ctor in a potential delegation chain] has returned, and whose dtor has not been called). This explicit statement can be rephrased as: B' is a completely constructed object, whose dtor is invoked if an exception occurs in B execution block. There is not a single mention or implication of member functions. – okovko Oct 15 '15 at 21:08
  • Clarifying the rest: the implementation may just write on the same stack address (no ctors required, that was a blunder) when it's dealing with trivial objects, regardless of what they've deleted. This is a potential optimization for very simple cases I speculate a sane implementation will perform. There is still overhead because the same data per ctor is overwritten in the stack, sort of like overhead due to a function call. The implementation could perhaps avoid rewriting the data, but that requires technically disobeying the standard. Probably, that is the case. Only for trivial types! – okovko Oct 15 '15 at 21:48
  • The fact that a destructor CAN be called implies that the object is fully constructed. In the first sentence you call B and B' ctors, in the next you say they're objects. I'm tired of going into pedantic details of this. You seem to have completely misinterpreted the wikipedia text which at no point mentions copies. I've tried to explain it as best I can, taking a few moments from work since I thought the question was interesting. Your comments suggest that you think you know the subject matter well, so please suggest an edit to the answer. If I agree, I will accept it. – nishantjr Oct 16 '15 at 03:34
  • I've been meandering in comments. Apologies. See my answer. I wouldn't say I've misinterpreted the Wikipedia text. I would say you're avoiding addressing your non sequitur and also not focusing on the more important source, the standard itself. – okovko Oct 16 '15 at 06:28
  • @okovko Personally I see non-sequiturs in posts by you here. The nishantjr's choice of word "like an ordinary member function" may have been confusing. But that's about it. – sehe Oct 16 '15 at 07:09
  • You were, and are still wrong. Check the latest answer. – okovko Mar 25 '20 at 02:42
3

Chained delegating constructors in C++11 do incur more overhead than the C++03 init function style!

See C++11 standard draft N3242, section 15.2. An exception may occur in the execution block of any link in the delegation chain, and C++11 extends existing exception handling behavior to account for that.

[text] and emphasis mine.

An object of any storage duration whose initialization or destruction is terminated by an exception will have destructors executed for all of its fully constructed subobjects ..., that is, for subobjects for which the principal constructor (12.6.2) has completed execution and the destructor has not yet begun execution. Similarly, if the non-delegating constructor for an object has completed execution and a delegating constructor for that object exits with an exception, the object’s [treated like a subobject as above] destructor will be invoked.

This is describing delegating ctors' consistency with the C++ object stack model, which necessarily introduces overhead.

I had to get familiar with things like how the stack works on a hardware level, what the stack pointer is, what automatic objects are, and what stack unwinding is, to really understand how this works. Technically these terms/concepts are implementation defined details, so N3242 does not define any of these terms; but it does use them.

The gist of it: Objects declared on the stack are allocated onto memory, and the executable handles the addressing and cleanup for you. The implementation of the stack was simple in C, but in C++, we have exceptions, and they demand an extension of C's stack unwinding. Section 5 of a paper by Stroustrup* discusses the need for extended stack unwinding, and the necessary additional overhead introduced by such a feature:

If a local object has a destructor, that destructor must be called as part of the stack unwinding. [A C++ extension of stack unwinding for automatic objects requires] ...an implementation technique that (in addition to the standard overhead of establishing a handler) involves only minimal overhead.

It's this very implementation technique and overhead that you add into your code for every link in your delegation chain. Every scope has the potential for an exception, and every constructor has its own scope, so every constructor in the chain adds overhead (as compared to an init function that only introduces one additional scope).

It's true that the overhead is minimal, and I'm sure that sane implementations optimize simple cases to remove that overhead. However, consider a case where you've got a 5 class inheritance chain. Let's say each of these classes has 5 constructors, and within each class, these constructors call each other in a chain to reduce redundant coding. If you instantiate an instance of the most derived class, you will incur the above described overhead up to 25 times, whereas the C++03 version would've incurred that overhead up to 10 times. If you make these classes virtual and multiply inheriting, this overhead will increase related to the accumulation of those features, as well as those features themselves introducing additional overhead. The moral here, is that as your code scales, you will feel the bite of this new feature.

*The Stroustrup reference was written a long time ago, to motivate discussion on C++ exception handling and defines potential (not necessarily) C++ language features. I chose this reference over some implementation specific reference because it is human readable, and 'portable.' My core use of this paper is section 5: specifically the discussion of the need for C++ stack unwinding, and the necessity of its overhead incurrence. These concepts are legitimized within the paper, and are valid today for C++11.

okovko
  • 1,851
  • 14
  • 27
  • Thank you for pointing that out. I rephrased that paragraph to express my intent: I sure as hell did not know these things before I researched them, and I needed a grasp of them to understand what the standard is saying. I would assume there are some other imaginary people like me out there :P – okovko Oct 16 '15 at 11:51
  • Thanks for that. I'm not sure I can come up with an example of a situation where "you will feel the bite of this new feature". Could you come up with one (I wager that compilers can use implicit noexcept-ness and empty constructor bodies easily. In fact, constructors inline and there might be specific optimizations for nested exception handling scopes. I'm sceptic about "the bite" unless we assume [Hell++](https://www.google.nl/search?q=%2Bsite:stackoverflow.com+%22hell%2B%2B%22)) – sehe Oct 16 '15 at 12:28
  • Modern implementations of exception handling often use a table-based approach that has zero (direct) runtime overhead on the non-exceptional code path. Additionally, I agree with sehe that inlining and inter-function optimizations can be applied; typically all constructors of a class are defined in the same file anyway. Please, if you claim that there's an *actual* overhead and not just the possibility of an overhead, provide an example plus measurements. – dyp Oct 17 '15 at 10:36
2

Class constructors have two parts, a member initializer list and a function body. With constructor delegation, the initializer list and function body of the delegated (target) constructor is first executed. After that, the function body of the delegating constructor is executed. You may, in certain cases, consider an object to be fully constructed when both the initializer list and the function body of some constructor are executed. That's why the wiki says each delegating constructor will be executing on a fully constructed object of its own type. In fact, the semantics can be more accurately described as:

...the function body of each delegating constructor will be executing on a fully constructed object of its own type.

However, the delegated constructor may only partially construct the object, and is designed to be invoked by other constructors only other than to be used alone. Such a constructor is usually declared private. So, it may not always be appropriate to consider the object to be fully constructed after the execution of the delegated constructor.

Anyway, since only a single initializer list is performed, there is no such overhead as you have mentioned. Following are quoted from cppreference:

If the name of the class itself appears as class-or-identifier in the member initializer list, then the list must consist of that one member initializer only; such constructor is known as the delegating constructor, and the constructor selected by the only member of the initializer list is the target constructor

In this case, the target constructor is selected by overload resolution and executed first, then the control returns to the delegating constructor and its body is executed.

Delegating constructors cannot be recursive.

Lingxi
  • 14,579
  • 2
  • 37
  • 93
  • I think the "fully constructed" portion is misleading. The total object is not fully constructed until the last constructor is finished. Each delegated constructor is constructing a portion of the total object. A delegating constructor constructs its portion of the object after the delegated constructor is finished constructing its portion first. So what has been "fully constructed" when a delegated constructor is executed? Maybe the reference actually means "fully allocated waiting to be constructed" instead? – Remy Lebeau Oct 14 '15 at 03:22
  • @RemyLebeau Agreed. The delegated constructor may only partially construct the object, and is designed to be invoked by other constructors only rather than to be used alone. Such a constructor may be declared private. Gonna update the answer. Thanks for the comments. – Lingxi Oct 14 '15 at 03:29
  • The cppreference source you provided is irrelevant and does not address the internal behavior of delegation chains. You're using yourself as a source, inventing information that you hope is true, but is not verified. Please don't answer questions this way! http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n1986 There must be complete objects formed along the delegation chain because of exceptions. If, like you and Lebeau suggest, the object is partially defined by successive delegated ctors, then an exception within the delegating ctor would destruct a nonexistent object. – okovko Oct 14 '15 at 04:00
  • 1
    If you read the original proposal I cited, you'll find that what actually happens is that a complete object is created per delegated ctor, so that an exception within a ctor execution block will destruct the last created object. – okovko Oct 14 '15 at 04:02
  • @okovko The key point is **since only a single initializer list is performed, there is no such overhead as you have mentioned**. – Lingxi Oct 14 '15 at 04:32
  • 1
    There's a discrepancy between lifetime, destruction and initialization in the Standard. The only thing resembling a definition of "fully constructed" I can find in the C++11 IS is [except.ctor]p2 *"[...] will have destructors executed for all of its fully constructed subobjects [...], that is, for subobjects for which the principal constructor has completed execution and the destructor has not yet begun execution."* OTOH, the same paragraph states that the dtor is called when the delegating ctor exits via exception. cc @RemyLebeau – dyp Oct 14 '15 at 05:27
  • 1
    @dyp. And that meanst that the destructor will be called if a constructor throws, if and only if at least one constructor has completed. – Johan Lundberg Oct 14 '15 at 20:51
  • @dyp Props for actually researching the topic. – okovko Oct 15 '15 at 20:58
  • @RemyLebeau See dyp's comment. Your definition of fully constructed is incorrect. Read the standard! – okovko Oct 15 '15 at 20:58
  • You were, and are still wrong. Check the latest answer. – okovko Mar 25 '20 at 02:42
1

The overhead is measurable. I implemented the following main-function with the Player-class and ran it several times with the delegating constructor as well as with the constructor with the init function (commented out). I built the code with g++ 7.5.0 and different optimization levels.

Build command: g++ -Ox main.cpp -s -o file_g++_Ox_(init|delegating).out

I ran every program five times and calculated the average on a Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz

Runtimes in msec:

Opt-Level | delegating | init

-O0 | 40966 | 26855

-O2 | 21868 | 10965

-O3 | 6475 | 5242

-Ofast | 6272 | 5123

To construct 50,000! objects is probably not a usual case, but there is a overhead for delegation constructors and that was the question.

#include <chrono>

class Player
{
private:
    std::string name;
    int health;
    int xp;
public:
    Player();
    Player(std::string name_val, int health_val, int xp_val);
};

Player::Player()
    :Player("None", 0,0){
}

//Player::Player()
//        :name{"None"}, health{0},xp{0}{
//}

Player::Player(std::string name_val, int health_val, int xp_val)
    :name{name_val}, health{health_val},xp{xp_val}{

}

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 50000; i++){
        Player player[i];
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start ).count();

    std::cout << duration;

    return 0;
}
Runms
  • 58
  • 7
  • Can you also note your CPU, complete compilation command, and also show what you got with different optimization levels? – okovko Mar 22 '20 at 22:40
  • I'm not sure this answer is appropriate for the question. This answer is for a particular compiler. The question seems focused on whether the language *requires* additional overhead, rather than whether some implementation causes it. That some compiler doesn't optimize delegated constructors as well as another isn't the same as the feature itself *causing* overhead. Also, if you're going to compare delegate constructors, it needs to be compared to an equivalent case. That is, when your constructor *explicitly* calls some function to fill in the members. – Nicol Bolas Mar 27 '20 at 15:23
  • In case of a delegating constructor you construct std::string twice which makes for the overhead you observe. – kozmo Jun 17 '20 at 10:06