Почему конструкторы перемещения неявно удаляются для моего класса?

Следующий код...

// main.cpp
#include <grpcpp/grpcpp.h>

class ClientContextContainer {
public:
  ClientContextContainer( int i ) : i_(i) {}

private:
  grpc::ClientContext client_context_;
  int i_;
};

class ArrayContainer {
public:
  ArrayContainer() : ccc_{ {42}, {1138} } {}

private:
  ClientContextContainer ccc_[2];
};

int main( int argc, char* argv[] ) {
  ArrayContainer ac;

 return 0;
}

... генерирует эту ошибку:

root@178258c7c52d:/tmp# /usr/bin/x86_64-linux-gnu-g++ --version && /usr/bin/x86_64-linux-gnu-g++ -c ./main.cpp
x86_64-linux-gnu-g++ (Debian 8.3.0-6) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

./main.cpp: In constructor 'ArrayContainer::ArrayContainer()':
./main.cpp:15:41: error: use of deleted function 'ClientContextContainer::ClientContextContainer(ClientContextContainer&&)'
   ArrayContainer() : ccc_{ {42}, {1138} } {}
                                         ^
./main.cpp:4:7: note: 'ClientContextContainer::ClientContextContainer(ClientContextContainer&&)' is implicitly deleted because the default definition would be ill-formed:
 class ClientContextContainer {
       ^~~~~~~~~~~~~~~~~~~~~~
./main.cpp:4:7: error: 'grpc::ClientContext::ClientContext(const grpc::ClientContext&)' is private within this context
In file included from /usr/local/include/grpcpp/client_context.h:37,
                 from /usr/local/include/grpcpp/grpcpp.h:53,
                 from ./main.cpp:2:
/usr/local/include/grpcpp/impl/codegen/client_context.h:388:3: note: declared private here
   ClientContext(const ClientContext&);
   ^~~~~~~~~~~~~

Я не совсем понимаю ошибку компилятора, но, судя по моему чтению, проблема заключается в том, что конструктор перемещения class ClientContextContainer неявно удаляется, потому что объект класса grpc::ClientContext имеет закрытый конструктор копии. (Осмотр client_context.h показывает, что его оператор присваивания также является закрытым).

Отлично. Но если это так, почему я не могу воспроизвести ошибку компиляции, если заменю объект-член grpc::ClientContext своим собственным объектом, который также имеет закрытый конструктор копирования (и оператор присваивания)?

// main.cpp
//#include <grpcpp/grpcpp.h>

class FauxClientContext {
public:
  FauxClientContext() {}

private:
  FauxClientContext( const FauxClientContext& );
  FauxClientContext& operator=( const FauxClientContext& );
};

class ClientContextContainer {
public:
  ClientContextContainer( int i ) : i_(i) {}

private:
  FauxClientContext client_context_;
  //grpc::ClientContext client_context_;
  int i_;
};

class ArrayContainer {
public:
  ArrayContainer() : ccc_{ {42}, {1138} } {}

private:
  ClientContextContainer ccc_[2];
};

int main( int argc, char* argv[] ) {
  ArrayContainer ac;

 return 0;
}
root@178258c7c52d:/tmp# /usr/bin/x86_64-linux-gnu-g++ --version && /usr/bin/x86_64-linux-gnu-g++ -c ./main.cpp
x86_64-linux-gnu-g++ (Debian 8.3.0-6) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

root@178258c7c52d:/tmp#

Какова истинная природа ошибки при компиляции первой версии main.cpp? Тот факт, что мне удалось успешно скомпилировать вторую версию main.cpp, подразумевает, что это не то, что я интерпретировал сообщение об ошибке компилятора.


Для тех, кто знаком с grpc, корень этой строки вопроса таков: можно ли создать массив объектов в списке инициализаторов, если объекты имеют элемент grpc::ClientContext объект?


person StoneThrow    schedule 04.12.2020    source источник
comment
В прежние времена, до C++11, одним из лучших способов построения блочной копии было создание конструктора копирования private, а не его реализация. Похоже, что grpc::ClientContext не предназначен для копирования.   -  person user4581301    schedule 04.12.2020
comment
Теперь я думаю, вопрос в том, почему один запускает конструктор копирования, а другой нет? Не знаю. Если бы я ответил, я бы ответил.   -  person user4581301    schedule 04.12.2020
comment
Вы можете обнаружить, что добавление строки ClientContextContainer(ClientContextContainer&&) = delete; к вашему определению ClientContextContainer проясняет и/или озадачивает. То есть вместо того, чтобы пытаться сделать конструктор перемещения неявно удаленным, сделайте его явным удаленным. Спойлер: он все еще компилируется.   -  person JaMiT    schedule 04.12.2020
comment
@JaMit Для g++ (с -std=c++17 -O2 -Wall -pedanti) он не будет компилироваться, как только вы введете нетривиальный член в FauxClientContext (как это сделал класс clientContext grpc), но он будет с MSBuild По крайней мере, несоответствие С++ 17.   -  person Secundi    schedule 05.12.2020
comment
Это вопрос о grpc или вообще о конструкторах перемещения?   -  person einpoklum    schedule 06.12.2020
comment
Это ошибка компилятора. Я изменил свой ответ, и теперь все должно быть ясно.   -  person Secundi    schedule 06.12.2020


Ответы (2)


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

Когда конструкторы перемещения неявно удаляются:

Выдержка из https://en.cppreference.com/w/cpp/language/move_constructor< /а>

Неявно объявленный или заданный по умолчанию конструктор перемещения для класса T определяется как удаленный, если выполняется любое из следующих условий:

  • T имеет нестатические элементы данных, которые нельзя перемещать (имеют удаленные, недоступные или неоднозначные конструкторы перемещения);
  • T имеет прямой или виртуальный базовый класс, который нельзя переместить (имеет удаленные, недоступные или неоднозначные конструкторы перемещения);
  • T имеет прямой или виртуальный базовый класс с удаленным или недоступным деструктором;
  • T является классом, подобным объединению, и имеет вариант члена с нетривиальным конструктором перемещения.

Конструктор перемещения по умолчанию, который удаляется, игнорируется разрешением перегрузки (в противном случае это предотвратит инициализацию копирования из rvalue).

Почему возникает ошибка компиляции:

В соответствии со стандартом, по крайней мере, уже с С++ 11, вы не должны на самом деле, поскольку вы правильно используете синтаксис инициализации прямого списка С++ 11 (см. ваш массив) с четко определенным конструктором для вашего случая. Конструкторы копирования и перемещения здесь не требуются, по крайней мере, для вашего случая. g++ имеет до сих пор не исправленную ошибку (сейчас почти историческую), которая вызывает ваши проблемы здесь, см.

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63707

Ваш компилятор работает стандартно (насколько я смог его протестировать), по крайней мере, до тех пор, пока ваш класс ClientContextContainer не содержит член, соответствующий деструктор класса которого не определен неявно (по умолчанию). Это не относится к grpc::ClientContext, так как он имеет несколько членов (общих указателей) с деструкторами, отличными от деструкторов по умолчанию. Вот почему ваш измененный фрагмент кода с этим простым классом хорошо работает даже в случае, когда все конструкторы копирования/перемещения являются закрытыми/удаленными. Попробуйте ввести здесь критический член с точки зрения деструктора, и вы сможете быстро воспроизвести ошибку компиляции.

Примечание:

Ваша фактическая проблема здесь не имеет ничего общего с гарантированным удалением копии, представленным в С++ 17 (на самом деле это было мое ошибочное предположение). Возможно, это имеет косвенное значение здесь с акцентом на эту ошибку g ++, поскольку, если ошибка представляет собой отсутствие прямой инициализации, но незаконное использование инициализации копирования, g ++ также нарушит требование стандарта о гарантированном исключении копии, поскольку C ++ 17 здесь для вашего случая, я думаю.

Для получения дополнительной информации см.

https://en.cppreference.com/w/cpp/language/direct_initialization

https://en.cppreference.com/w/cpp/language/list_initialization

https://en.cppreference.com/w/cpp/language/copy_initialization

https://en.cppreference.com/w/cpp/language/copy_elision

person Secundi    schedule 05.12.2020

tl;dr: это ошибка компилятора.

Это ошибка в компиляторе GCC:

Инициализация массива с фигурными скобками иногда завершается ошибкой, если нет конструктора копирования (gcc.gnu .org/багзилла)

На момент написания статьи это было исправлено для GCC 11.0, 10.3 и 9.4, но проявляется в выпущенных в настоящее время версиях до 10.2 включительно.

Сокращенный тестовый пример Джонатана Уэйкли:

struct NonCopyable {
  NonCopyable(const NonCopyable&) = delete;
  NonCopyable(NonCopyable&&) = delete;
  NonCopyable& operator=(const NonCopyable&) = delete;
  NonCopyable& operator=(NonCopyable&&) = delete;

  NonCopyable() {}

  ~NonCopyable() {} // makes the destructor non-trivial
};

struct A {
  A(): _a{} {}
  ~A() {}

  NonCopyable _a[5];
} a;

Это работает с clang, не работает с GCC (см. это на GodBolt).

person einpoklum    schedule 15.01.2021
comment
Спасибо за обновление! Только теория: Эту поторопились починить, так как Петр Димов из Boost упомянул и там свою встречу с этой... :) - person Secundi; 06.02.2021
comment
@Секунди: Что? Я думал, что это все на моем счету :-P - person einpoklum; 06.02.2021