Деструктор аргумента функции вызывается по-разному в gcc и MSVC

При переносе некоторого кода на С++ из Microsoft Visual Studio в gcc я столкнулся с странной ошибкой, которая в конечном итоге сводилась к следующему:

#include <iostream>
using namespace std;

class Foo {
public:
    int data;
    Foo(int i) : data(i) 
    {
        cout << "Foo constructed with " << i << endl; 
    }
    Foo(const Foo& f) : data(f.data)
    {
        cout << "copy ctor " << endl;
    }
    Foo(const Foo&& f) : data(f.data)
    {
        cout << "move ctor" << endl;
    }
    ~Foo()
    {
        cout << "Foo destructed with " << data << endl;
    }
};

int Bar(Foo f)
{
    cout << "f.data = " << f.data << endl;
    return f.data * 2;
}

int main()
{
    Foo f1(10);
    Foo f2(Bar(std::move(f1)));
}

Если я компилирую и запускаю вышеуказанный код с помощью сообщества Microsoft Visual Studio 2015, я получаю следующий вывод:

Foo constructed with 10
move ctor
f.data = 10
Foo destructed with 10
Foo constructed with 20
Foo destructed with 20
Foo destructed with 10

Однако, если я компилирую и запускаю код с gcc 6.1.1 и -std = С++ 14, я получаю этот вывод:

Foo constructed with 10
move ctor
f.data = 10
Foo constructed with 20
Foo destructed with 10
Foo destructed with 20
Foo destructed with 10

gcc вызывает деструктор f, аргумент Bar(), после Bar() возвращает, а msvc вызывает деструктор (по-видимому) до его возвращения или, по крайней мере, перед конструктором f2. Когда f предполагается разрушить, согласно стандарту С++?

Ответ 1

Все в порядке; это зависит. Кажется, это указано в стандарте.

Из [expr.call]/4 (эта формулировка восходит к С++ 98);

Время жизни параметра заканчивается, когда возвращается функция, в которой она определена. Инициализация и уничтожение каждого параметра происходит в контексте вызывающей функции.

И CWG # 1880;

WG решила не указывать, уничтожены ли объекты параметров сразу после вызова или в конце полного выражения, к которому принадлежит вызов.

Как поведение g++ (и clang), так и MSVC было бы правильным, реализации могут выбирать один подход по сравнению с другим.

Все сказанное, если код, который у вас есть, зависит от этого заказа, я бы изменил его таким образом, чтобы упорядочение было более детерминированным - как вы видели, это приводит к тонким ошибкам.


Упрощенный пример такого поведения:

#include <iostream>
struct Arg {
    Arg() {std::cout << 'c';}
    ~Arg() {std::cout << 'd';}
    Arg(Arg const&)  {std::cout << 'a';}
    Arg(Arg&&)  {std::cout << 'b';}
    Arg& operator=(Arg const&)  {std::cout << 'e'; return *this;}
    Arg& operator=(Arg&&)  {std::cout << 'f'; return *this;}
};
void func(Arg) {}
int main() {
    (func(Arg{}), std::cout << 'X');
    std::cout << std::endl;
}

Clang и g++ производят cXd, а MSVC производит cdX.