Является ли предупреждение -Wreturn-std-move clang корректным в случае объектов в одной иерархии? - PullRequest
3 голосов
/ 30 апреля 2019

Рассмотрим следующий простой код:

struct Base
{
  Base() = default;      
  Base(const Base&);      
  Base(Base&&);
};

struct Derived : Base { };

Base foo()
{
  Derived derived;
  return derived;
}

лязг 8.0.0 выдает предупреждение -Wreturn-std-move на нем:

prog.cc:21:10: warning: local variable 'derived' will be copied despite being returned by name [-Wreturn-std-move]
  return derived;
         ^~~~~~~
prog.cc:21:10: note: call 'std::move' explicitly to avoid copying
  return derived;
         ^~~~~~~
         std::move(derived)

Но если вызвать здесь std::move, то поведение кода может измениться, потому что подобъект Base объекта Derived будет перемещен до вызова деструктора объекта Derived и кода объекта. последний будет вести себя по-другому.

например. посмотрите на код (скомпилированный с флагом -Wno-return-std-move) :

#include <iostream>
#include <iomanip>

struct Base
{
  bool flag{false};

  Base()
  {
    std::cout << "Base construction" << std::endl;
  }

  Base(const bool flag) : flag{flag}
  {
  }

  Base(const Base&)
  {
    std::cout << "Base copy" << std::endl;
  }

  Base(Base&& otherBase)
  : flag{otherBase.flag}
  {
    std::cout << "Base move" << std::endl;
    otherBase.flag = false;
  }

  ~Base()
  {
    std::cout << "Base destruction" << std::endl;
  }
};

struct Derived : Base
{
  Derived()
  {
    std::cout << "Derived construction" << std::endl;
  }

  Derived(const bool flag) : Base{flag}
  {
  }

  Derived(const Derived&):Base()
  {
    std::cout << "Derived copy" << std::endl;
  }

  Derived(Derived&&)
  {
    std::cout << "Derived move" << std::endl;
  }

  ~Derived()
  {
    std::cout << "Derived destruction" << std::endl;
    std::cout << "Flag: " << flag << std::endl;
  }
};

Base foo_copy()
{
  std::cout << "foo_copy" << std::endl;
  Derived derived{true};
  return derived;
}

Base foo_move()
{
  std::cout << "foo_move" << std::endl;
  Derived derived{true};
  return std::move(derived);
}

int main()
{
  std::cout << std::boolalpha;
  (void)foo_copy();
  std::cout << std::endl;
  (void)foo_move();
}

Его вывод:

foo_copy
Base copy
Derived destruction
Flag: true
Base destruction
Base destruction

foo_move
Base move
Derived destruction
Flag: false
Base destruction
Base destruction

Ответы [ 3 ]

4 голосов
/ 30 апреля 2019

Является ли предупреждение -Wreturn-std-move clang корректным в случае объектов в той же иерархии?

Да, предупреждение верно. Текущие правила автоматического перемещения выполняются только в том случае, если при разрешении перегрузки обнаружен конструктор, который принимает , в частности, , и ссылается на rvalue для этого типа. В этом фрагменте:

Base foo()
{
  Derived derived;
  return derived;
}

derived - это объект автоматического хранения, который возвращается - он все равно умирает, поэтому с него безопасно двигаться. Поэтому мы пытаемся это сделать - мы рассматриваем это как значение и находим Base(Base&&). Это жизнеспособный конструктор, но он требует Base&& - и нам нужно очень конкретно a Derived&&. Так что в конечном итоге копирование.

Но копия расточительна. Зачем копировать, когда derived выходит из области видимости? Зачем использовать дорогую операцию, если вы можете использовать дешевую? Вот почему есть предупреждение, чтобы напомнить вам:

Base foo()
{
  Derived derived;
  return std::move(derived); // ok, no warning
}

Теперь, если нарезка является неправильной для этой иерархии, то даже копирование делает неправильную вещь в любом случае, и у вас есть другие проблемы. Но если нарезка является приемлемой, то вы хотите переместиться сюда, а не копировать, и язык в данный момент, возможно, делает неправильную вещь. Это предупреждение поможет вам сделать все правильно.

4 голосов
/ 30 апреля 2019

Предупреждение Clang, безусловно, верно.Поскольку derived относится к типу, который отличается от типа, возвращаемого функцией, в операторе return derived; компилятор должен рассматривать derived как lvalue, и будет происходить его копирование.И этой копии можно избежать, написав return std::move(derived);, явным образом превратив ее в значение.Предупреждение не говорит вам, должны ли вы сделать это.Он просто говорит вам о последствиях того, что вы делаете, и последствиях использования std::move, и позволяет вам принять собственное решение.

Вы беспокоитесь о том, что деструктор Derived может получить доступ кBase состояние после перемещения, что может привести к ошибкам.Если такие ошибки возникают, то это потому, что автор Derived допустил ошибку, а не потому, что пользователь не должен был перемещать подобъект Base.Такие ошибки могут быть обнаружены так же, как и другие ошибки, и сообщены автору Derived.

Почему я так говорю?Поскольку, когда автор сделал Base общедоступный базовый класс Derived, он пообещал пользователю, что он имеет право использовать полный интерфейс Base всякий раз, когда взаимодействует с объектом Derived,что включает в себя перемещение от него.Таким образом, все функции-члены Derived должны быть готовы к тому, что пользователь мог модифицировать подобъект Base любым способом, который позволяет интерфейс Base.Если это нежелательно, то Base можно сделать частным базовым классом Derived или частным элементом данных, а не общедоступным базовым классом.

2 голосов
/ 30 апреля 2019

Обычно рекомендуется, чтобы единственными классами в вашей иерархии, которые не являются абстрактными, были листовые классы. Все, что используется как полиморфный базовый класс, должно быть абстрактным.

Это сделало бы исходный код (о котором предупреждает кланг) в первую очередь недопустимым, поскольку вы не смогли бы вернуть Base по значению. И действительно, оригинальный код оставляет читателю множество вопросов, в первую очередь потому, что он нарушает это правило:

  • Какой смысл создавать Derived и возвращать Base только по значению?

  • Есть ли здесь срезы объектов , происходящие здесь? Может ли это случиться в будущем, если кто-то добавит код в один из классов?

  • В связи с этим, как вы хотите применить свои инварианты классов, если ваши классы не являются полиморфными (нет виртуальных деструкторов, если назвать одну проблему)?

  • Чтобы соответствовать принципу подстановки Лискова, либо все виды производных классов должны позволять удалять свои субобъекты Base, либо ни один из них. В последнем случае это можно предотвратить, удалив конструктор перемещения Base. В первом случае с предупреждением проблем нет.

  • Насколько замысловатыми должны быть инварианты вашего класса, чтобы уничтожить Base само по себе хорошо, уничтожить Derived с его Base подарком - это хорошо, но уничтожить Derived без его Base не будет хорошо? Обратите внимание, что это практически невозможно, если вы следуете правилу , равному нулю .

Так что да, можно написать код, в котором использование std::move, как предлагает clang, меняет значение. Но этот код уже должен был бы нарушать многие принципы кодирования. Я не думаю, что разумно ожидать, что компилятор предупреждений будет уважать эту возможность.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...