Vector:: push_back настаивает на использовании конструктора копирования, хотя предусмотрен механизм перемещения

Я получал странную ошибку от gcc и не могу понять, почему. Я сделал следующий примерный код, чтобы сделать проблему более ясной. В принципе, существует определенный класс, для которого я делаю его конструктором копирования и оператором присваивания копии частным, чтобы случайно вызвать их.

#include <vector>
#include <cstdio>
using std::vector;

class branch 
{
public:
  int th;

private:
  branch( const branch& other );
  const branch& operator=( const branch& other );

public:

  branch() : th(0) {}

  branch( branch&& other )
  {
    printf( "called! other.th=%d\n", other.th );
  }

  const branch& operator=( branch&& other )
  {
    printf( "called! other.th=%d\n", other.th );
    return (*this);
  }

};



int main()
{
  vector<branch> v;
  branch a;
  v.push_back( std::move(a) );

  return 0;
}

Я ожидаю, что этот код будет компилироваться, но он не работает с gcc. На самом деле gcc жалуется, что "branch:: branch (const branch &) является частной", что, как я понимаю, не следует вызывать.

Оператор присваивания работает, так как если я заменил тело main() на

branch a;
branch b;
b = a;

Он будет компилироваться и выполняться как ожидалось.

Это правильное поведение gcc? Если да, то что не так с вышеуказанным кодом? Любое предложение полезно для меня. Спасибо!

Ответ 1

Попробуйте добавить "noexcept" к объявлению конструктора перемещения.

Я не могу привести этот стандарт, но последние версии gcc требуют, чтобы либо конструктор копирования был общедоступным, либо конструктор перемещения был объявлен "noexcept" . Независимо от квалификатора "noexcept" , если вы сделаете публичный конструктор копирования, он будет вести себя так, как вы ожидаете, во время выполнения.

Ответ 2

В отличие от предыдущего ответа, gcc 4.7 неправильно отклонил этот код, ошибка, которая была исправлена ​​в gcc 4.8.

Полное стандартное поведение для vector<T>::push_back:

  • Если есть только конструктор копирования и конструктор перемещения, push_back скопирует его аргумент и предоставит надежную гарантию безопасности. То есть, если push_back не удается из-за исключения, вызванного перераспределением векторного хранилища, исходный вектор останется неизменным и применимым. Это известное поведение из С++ 98, и это также причина беспорядка, который следует.
  • Если существует конструктор перемещения noexcept для T, push_back переместится из его аргумента и предоставит надежную гарантию исключения. Здесь нет сюрпризов.
  • Если есть конструктор перемещения, который не является noexcept, и есть также конструктор копирования, push_back скопирует объект и предоставит надежную гарантию безопасности. Это неожиданно на первый взгляд. Хотя push_back может двигаться здесь, это было бы возможно только за счет жертвования надежной гарантии исключения. Если вы портировали код с С++ 98 на С++ 11, и ваш тип движимый, это тихо изменило бы поведение существующих вызовов push_back. Чтобы избежать этой ошибки и поддерживать совместимость с кодом С++ 98, С++ 11 возвращается к более медленной копии. Это поведение gcc 4.7. Но есть еще...
  • Если есть конструктор перемещения, который не является noexcept, но вообще не имеет конструктора копирования, т.е. элемент может быть перемещен и не скопирован - push_back выполнит этот шаг, но не даст сильного исключения гарантия. Здесь gcc 4.7 поступил не так. В С++ 98 нет push_back для типов, которые могут перемещаться, но не копироваться. Поэтому жертвовать сильной безопасностью исключений здесь не нарушает существующий код. Вот почему это разрешено, и исходный код на самом деле является законным С++ 11.

Смотрите cppreference.com в push_back:

Если выбрано исключение, эта функция не имеет эффекта (сильный исключение).

Если конструктор перемещения T не является несущественным, а экземпляр копирования недоступен, вектор будет использовать перемещение броска конструктор. Если он бросает, гарантия отменяется, а эффекты не определено.

Или немного более запутанный §23.3.6.5 из С++ 11 Standard (выделено мной мной):

Вызывает перераспределение, если новый размер больше старой. Если перераспределение не происходит, все итераторы и ссылки до точка ввода остается в силе. Если исключение выдается иначе, чем конструктором копирования, конструктором перемещения, оператором присваивания или переместить оператор назначения T или любую операцию ввода ввода не являются эффектами. Если исключение выбрано конструктором перемещения non-CopyInsertable T, эффекты не определены.

Или, если вам не нравится читать, Скотт Мейер, ведущий родной разговор 2013 года (начало в 0:30:20 с интересной частью примерно в 0:42: 00).