std::pair move not elided on definition?
Asked Answered
O

1

6

I noticed something very strange with Visual Studio 2012: Defining a pair object like so:

    auto objp = pair<int, LogMe>();

will not elide the copy/move of the pair in VC11, this call will print:

LogMe::LogMe - def.ctor!
LogMe::LogMe - move.ctor!
LogMe::~LogMe - dtor!

that is, a temporary pair will be created and then moved into the objp variable. (declaring it as pair<...> obj; only logs the default ctor)

I have cross checked with my LogMe test object alone:

cout << "# Construct Object via auto obj = ...\n";
auto obj = LogMe();

# Construct Object via auto obj = ...
LogMe::LogMe - def.ctor!

and here the assignment will be elided.

This seems to be specific to VC11, as testing it in IDEOne (which uses gcc 4.8.1) shows that the extraneous move is elided always there.

What's going on here? Not being able to rely on the initialization copy being elided makes me nervous.

Note: Tests for release vs. debug version show the same result. (Which I would have expected, as copy-elision is performed independently of optimization flags in MSVC.)


Full sourcecode to test (See also the ideone link):

#include "stdafx.h"
#include <iostream>
#include <map>

using namespace std;

struct LogMe {
    std::string member;

    LogMe() {
        cout << __FUNCTION__ << " - def.ctor!" << endl;
    }
    ~LogMe() {
        cout << __FUNCTION__ << " - dtor!" << endl;
    }
    LogMe(LogMe const&) {
        cout << __FUNCTION__ << " - cpy.ctor!" << endl;
    }
    LogMe& operator=(LogMe const&) {
        cout << __FUNCTION__ << " - cpy.assign.op!" << endl;
        return *this;
    }
    LogMe(LogMe&&) {
        cout << __FUNCTION__ << " - move.ctor!" << endl;
    }
    LogMe& operator=(LogMe&&) {
        cout << __FUNCTION__ << " - move.assign.op!" << endl;
        return *this;
    }
};

int _tmain(int argc, _TCHAR* argv[])
{
    {
        cout << "# Construct Object via auto obj = ...\n";
        auto obj = LogMe();
        cout << "# Construct pair<int, object> via auto objp = ...\n";
        auto objp = pair<int, LogMe>();
        cout << "# Construct pair<int, object> via pair objp2; ...\n";
        pair<int, LogMe> p2;
    }
    return 0;
Officinal answered 9/10, 2013 at 21:17 Comment(7)
I thought it might be that the pair constructor might be something like pair( T&& = type1(), U&& = type2() ) -- ie, it might be creating the temporary in creating the single pair. But that isn't the case -- there is an explicit trivial constructor. The next question would be, are you doing this in debug?Assessor
@Yakk - see my note. debug vs release doesn't make a differenceOfficinal
Bah, missed that. Does auto matter?Assessor
@Yakk - I think I saw that auto doesn't matter. I'll have to re-check once I get back to the machine with VC11.Officinal
Hmmm ... I tested this with VS2005 (without the moves and without the auto) and there the copy is also always elided.Officinal
as a crazy theory, maybe the move ctor confuses the vc11 elision optimizer? Should be checkable.Assessor
@Yakk see my answer. Do you happen to have access to a VS2013 to check whether this is also present in the newest version?Officinal
O
2

It appears it is not the move ctor, nor the templated move ctor that is causing the problem, but the presence of enable_if<is_convertable<... in the templated move ctor:

Testing with just an object, throwing auto and pair out of the test:

  • OK, copy/move elided:

            cout << "# Construct Object: auto obj = LogMe();\n";
            LogMe obj = LogMe();
    
            LogMe(LogMe&&) {
                cout << __FUNCTION__ ...
            }
    

And, with a test like so:

    cout << "# Construct Object: LogMeTempl obj = LogMeTempl();\n";
    LogMeTempl obj = LogMeTempl();
    cout << "# Construct Object: LogMeTempl obj2;\n";
    LogMeTempl obj2;
  • OK, copy move also elided:

    template<class Other>
    LogMeTempl(Other&& rhs
    //      , typename enable_if<is_convertible<Other, LogMeTempl>::value, void>::type ** = 0
    ) {
        cout << __FUNCTION__ << ...;
    }
    
  • Fail! Move ctor invoked!

    template<class Other>
    LogMeTempl(Other&& rhs
            , typename enable_if<is_convertible<Other, LogMeTempl>::value, void>::type ** = 0
    ) {
        cout << __FUNCTION__ << ...;
    }
    

    And note that the enable_if can be reduced to enable_if<true, void>::type** = 0 - if fact any additional defaulted parameter will do (e.g. , int defaulted_param_on_move_ctor = 0 and it will still prevent the move elision).

    This also extends to a type with a copy-ctor only that has a default argument. It won't be elided either. A quick cross-check with gcc shows there doesn't seem to be any such problem there.

Short Answer

Types with defaulted arguments in their copy/move ctor don't have their initialization copy/move elided.

I have added a bug on MS.connect for this issue.

I have also added a test-case for (N)RVO to IDEone. Even without the default argument, *N*RVO seems to work better in gcc than VC++.

Officinal answered 10/10, 2013 at 19:30 Comment(1)
Neat! So two problems: first, they have to use the hacky "default nullptr with contrived type" to enable SFINAE, and second because the elision optimization is blocked by additional default parameters (incorrectly, I think). In C++11 (and in MSVC2013) they can use default template parameters in the ctor to do this without a default argument to the constructor. I do not know if the MSVC2013 std C++ library has this improvement or not, but MSVC 2013 has compiler support for it blogs.msdn.com/b/vcblog/archive/2013/06/28/…Assessor

© 2022 - 2024 — McMap. All rights reserved.