Is default constructor elision / assignment elision possible in principle?
Asked Answered
W

4

7

. or even allowed by the C++11 standard?

And if so, is there any compiler that actually does it?

Here is an example of what I mean:

template<class T> //T is a builtin type
class data 
{
public:
    constexpr
    data() noexcept :
        x_{0,0,0,0}
    {}

    constexpr
    data(const T& a, const T& b, const T& c, const T& d) noexcept :
        x_{a,b,c,d}
    {}

    data(const data&) noexcept = default;

    data& operator = (const data&) noexcept = default;

    constexpr const T&
    operator[] (std::size_t i) const noexcept {
        return x_[i];
    }

    T&
    operator[] (std::size_t i) noexcept {
        return x_[i];
    }

private:
    T x_[4];
};


template<class Ostream, class T>
Ostream& operator << (Ostream& os, const data<T>& d)
{
    return (os << d[0] <<' '<< d[1] <<' '<< d[2] <<' '<< d[3]);
}


template<class T>
inline constexpr
data<T>
get_data(const T& x, const T& y)
{
    return data<T>{x + y, x * y, x*x, y*y};
}


int main()
{
    double x, y;
    std::cin >> x >> y;

    auto d = data<double>{x, y, 2*x, 2*y};

    std::cout << d << std::endl;

    //THE QUESTION IS ABOUT THIS LINE
    d = get_data(x,y);  

    d[0] += d[2];
    d[1] += d[3];
    d[2] *= d[3];

    std::cout << d << std::endl;

    return 0;
}

Regarding the marked line:
Could the values x+y, x*y, x*x, y*y be written directly to the memory of d? Or could the return type of get_data be directly constructed in the memory of d?
I can't think of a reason to not allow such an optimization. At least not for a class that has only constexpr constructors and default copy and assignment operators.

g++ 4.7.2 elides all copy constructors in this example; it seems however that assignment is always performed (even for default assignment only - as far as I can tell from the assembly that g++ emits).

The motivation for my question is the following situation in which such an optimization would greatly simplify and improve library design. Suppose you write performance-critical library routines using a literal class. Objects of that class will hold enough data (say 20 doubles) that copies have to be kept to a minimum.

class Literal{ constexpr Literal(...): {...} {} ...};

//nice: allows RVO and is guaranteed to not have any side effects
constexpr Literal get_random_literal(RandomEngine&) {return Literal{....}; }

//not favorable in my opinion: possible non-obvious side-effects, code duplication
//would be superfluous if said optimization were performed
void set_literal_random(RandomEngine&, Literal&) {...}

It would make for a much cleaner (functional programming style) design if I could do without the second function. But sometimes I just need to modify a long-lived Literal object and have to make sure that I don't create a new one and copy-assign it to the one I want to modify. The modification itself is cheap, the copies aren't - that's what my experiments indicate.

EDIT:
Let's suppose the optimization shall only be allowed for a class with noexcept constexpr constructors and noexcept default operator=.

Willmert answered 4/9, 2013 at 9:21 Comment(4)
As far as I know only copy/move-construction can be elided. The relevant part of the standard is 12.8/31. I couldn't find anything similar about copy/move-assignment elision.Selves
Me neither. That's why I'm interested to know if there are any principle objections to such an optimization.Coquito
That's why I added "At least not for a class that has only constexpr constructors and default copy and assignment operators". Which is precisely the case I care about.Coquito
A "little" hacky, but a 'placement new' would avoid the copy...Alderson
B
10

Elision of default copy/move assignment operators is allowed based on the general as-if rule only. That is the compiler can do it if it can ascertain that it will have no observable effect on behaviour.

In practice the as-if rule is used in general fashion to allow optimizations at intermediate representation and assembly levels. If the compiler can inline the default constructor and assignment, it can optimize them. It won't ever use the code of the copy constructor for it, but for their default implementations it should end up with the same code.

Edit: I answered before there was the code sample. Copy/move constructors are elided based on explicit permission to the compiler to do so, therefore they are elided even if they have observable effect (printing "COPY"). Assignments can only be elided based on as-if rule, but they have observable effect (printing "ASSIGN"), so the compiler is not allowed to touch them.

Bajaj answered 4/9, 2013 at 9:34 Comment(3)
Yes, OK. Maybe I should have removed my custom copy ctor and assignment operator in the example. The case I really care about is that you have only default copy ctors and operator=. And it seems based on the assembly that g++ emits that g++ does not elide the assignment/construction of the temporary.Coquito
@AndréMüller: Indeed. It could do it, but in practice it would be a very special rule that would take a lot of work to implement for little gain. So the only case when the temporary will get removed is when the generic "register" allocation finds it and it probably does not even try for non-trivial objects.Bajaj
Little gain? If one could rely on it beeing there in modern compilers it would eliminate the need to distinguish between in-place modification and construction for literal classes. Maybe I have done too much functional programming for wanting such a feature.Coquito
A
4

Does the standard allow for the elision of the assignment operator? Not in the same way as for construction. If you have any construct d = ..., the assignment operator will be called. If ... results in an expression of the same type as d, then the appropriate copy or move assignment operator will be called.

It is theoretically possible that a trivial copy/move assignment operator could be elided. But implementations are not allowed to elide anything that you could detect being elided.

Note that this is different from actual copy/move elision, because there the standard explicitly allows the elision of any constructor, whether trivial or not. You can return a std::vector by value into a new variable, and the copy will be elided if the compiler supports it. Even though it is very easy to detect the elision. The standard gives special permission to compilers to do this.

No such permission is granted for copy/move assignment. So it could only ever "elide" something that you couldn't tell the difference about. And that's not really "elision"; that's just a compiler optimization.

Objects of that class will hold enough data (say 20 doubles) that copies have to be kept to a minimum.

There's nothing stopping you from returning that Literal type right now. You will get elision if you store the object in a new variable. And if you copy assign it to an existing variable, you won't. But that's no different from a function which returns a float that you store into an existing variable: you get a copy of the float.

So it's really up to you how much copying you want to do.

Aynat answered 4/9, 2013 at 10:47 Comment(4)
OK, you are right, I should call it an optimization not an elision. But that's precisely the case I care about. I don't want any copy at all. I want the equivalent of in-place modification but without writing an extra function for that.Coquito
@AndréMüller: If the currently existing contents of the object are irrelevant, then you want to create a new object, not overwrite some current one. That will do the construction in-place. And if the currently existing contents of the object are relevant to the computation, then it's going to have to be passed as a parameter. You should not overwrite an existing object simply because it's there. That is, after all, what copying is.Aynat
It seems that I didn't make the use case clear enough. I have a long-lived object (encoding the current state of a simulation). One of the members of that object is like the one in the example. Now I want to update the simulation state by modifying the data of that member. But I don't want to create a second object, assign it to the first one and then destroy the second object. I want the compiler to optimize that away and give my the equivalent of in-place modification so I can use a factory function instead of having to write a separate function for modification.Coquito
@AndréMüller: This kind of optimization can only be done after your C++ code has been translated to a lower-level language. It probably will be optimized away at that point. If you want to control this at C++-level, your only hope is to add a data<T>& parameter to get_data to be able to state the "location of the so-called returned value". This is a decades-old trick.Kling
I
2

There is an important drawback to what you propose: what would happen if the constructor threw? The behaviour for that case is well defined in the standard (all data members that had already been constructed are destructed in reverse order), but how would that translate into the case where we are "constructing" the object into an already existing one?

Your example is easy because the constructor cannot throw when T = double, but this is not so in the general case. You might end up with a half destructed object and undefined behaviour would ensue, even if your constructor and assignment operator were well behaved.

Inferior answered 4/9, 2013 at 9:41 Comment(1)
Good point! That's exactly the kind of general issues I am interested in.Coquito
S
2

Jan Hudec's answer has a very good point about the as-if rule (+1 for him).

Hence, the assignment elision is allowed -- as he said -- provided that there's no observable effect. The fact that your assignment operator outputs "ASSIGN" is enough to prevent the optimization.

Notice that the situation is different for copy/move constructors because the standard allows elision for copy/move constructor even if they have observable side effects (see 12.8/31).

Selves answered 4/9, 2013 at 9:45 Comment(1)
Yes, but I care about the case with default operator=. I edited my original question, because the custom operator= created too much confusion.Coquito

© 2022 - 2024 — McMap. All rights reserved.