Запутанное взаимодействие между конструкторами и семантикой копирования/перемещения в C++

avatar
dboeger1
8 августа 2021 в 21:56
81
1
1

Исходное сообщение отредактировано для обеспечения более минимального воспроизведения.

Давний программист на C здесь, заново изучаю C++ впервые примерно с 2010 года, так что я совсем новичок во всем современном C++11 и выше. Я думаю, что понимаю намерение конструкторов копирования и перемещения и почему их можно явно определить/по умолчанию/удалить. Однако я совершенно не понимаю взаимодействия между различными типами конструкторов в моей собственной учебной программе:

.

TerritoryUSA.hpp

#ifndef TERRITORYUSA_HPP
#define TERRITORYUSA_HPP

namespace TerritoryUSA {
    enum class Identifier {
        Alabama,
        Alaska,
        American_Samoa
    };

    class Territory {
    private:
        Identifier identifier;
    public:
        // Construct.
        Territory(Identifier identifier);
        //Territory(Territory& other) = delete;         // 1
        //Territory(Territory&& other) = delete;        // 2

        // Move.
        //Territory& operator=(Territory& other) = delete;  // 3
        //Territory& operator=(Territory&& other) = delete; // 4

        // Destruct.
        ~Territory() = default;

        // Methods.
        Identifier getIdentifier();
    };
}

#endif /* TERRITORYUSA_HPP */

TerritoryUSA.cpp

#include "TerritoryUSA.hpp"

using namespace TerritoryUSA;

// Construct.
Territory::Territory(Identifier identifier) : identifier{identifier} {}

// Methods.
Identifier Territory::getIdentifier() {
    return identifier;
}

main.cpp

#include <cstdio>
#include "TerritoryUSA.hpp"

using namespace TerritoryUSA;

int main() {
    Territory territories[]{
        Territory(Identifier::Alabama),
        Territory(Identifier::Alaska),
        Territory(Identifier::American_Samoa)
    };

    for (Territory territory : territories) {
        printf("%d\n", static_cast<int>(territory.getIdentifier()));
    }
}

выход

0
1
2
Program ended with exit code: 0

Обратите внимание, что это делается в Xcode на MacOS, с чем я также пытаюсь разобраться, так как при написании C я в основном работаю с vim/make в Linux. Я не понял, где найти версию компилятора. пока, но это совершенно новый обновленный Macbook со свежеустановленным Xcode, поэтому он не может быть очень старым. Все, что я сделал, это запустил новое приложение Xcode «инструмент командной строки» и поместил 3 файла в каталог проекта, используя меню правой кнопки мыши, поэтому система сборки должна быть правильной (и она успешно собирается и работает, когда отмеченные строки прокомментированы ).

Часть, которая чертовски сбивает меня с толку, заключается в том, что когда я выборочно раскомментирую строки, отмеченные 1-4 в файле заголовка, я получаю различные ошибки, связанные с построением объектов Territory. Вот результаты, которые я получаю при раскомментировании отдельных строк:

Строка 1:

Строки 8-10 файла main.cpp (где я пытаюсь создать объекты Territory) я получаю эту красную ошибку:

No matching constructor for initialization of 'TerritoryUSA::Territory'

Он также выделяет серым цветом строку 17 hpp (конструктор, который принимает аргумент идентификатора) с этим сообщением:

1. Candidate constructor not viable: no known conversion from 'TerritoryUSA::Territory' to 'TerritoryUSA::Identifier' for 1st argument

Он также выделяет незакомментированную строку 1 серым цветом с этим сообщением:

2. Candidate constructor not viable: expects an l-value for 1st argument

Строка 2:

Строки 8-10 файла main.cpp (где я пытаюсь создать объекты Territory) я получаю эту красную ошибку:

Call to deleted constructor of 'TerritoryUSA::Territory'

Он также выделяет незакомментированную строку 2 серым цветом с этим сообщением:

1. 'Territory' has been explicitly marked deleted here

Строка 3:

Ошибок нет

Строка 4:

Строки 8-10 файла main.cpp (где я пытаюсь создать объекты Territory) я получаю эту красную ошибку:

Call to implicitly-deleted copy constructor of 'TerritoryUSA::Territory'

Он также выделяет незакомментированную строку 4 серым цветом с этим сообщением:

1. Copy constructor is implicitly deleted because 'Territory' has a user-declared move assignment operator

Вопросы

Для строки 1, почему она пытается использовать конструктор без аргументов и/или делать какие-либо неявные предположения? Разве я не предоставил совершенно правильный явный конструктор для подписи с 1 аргументом идентификатора? Я намеренно не хочу позволять пользователям создавать экземпляры объектов Territory без каких-либо аргументов (откровенно говоря, я не хочу, чтобы они создавали их вообще, но шаг за шагом). И еще, какое отношение к этому имеет удаление конструктора копирования? Я не пытаюсь ничего копировать.

Для строки 2, опять же, я понимаю, что удалил конструктор по умолчанию, но я не пытаюсь использовать его, я пытаюсь использовать тот, который принимает идентификатор. Почему я сбиваюсь с толку? И еще, какое отношение к этому имеет удаление конструктора перемещения? Я ничего не пытаюсь переместить.

Что касается строки 4, почему я получаю сообщение об ошибке, связанное с удаленным конструктором копирования, если я удаляю оператор перемещения? Я совершенно запутался в этом вопросе, особенно потому, что в строке 3 не было ошибок.

Источник
cigien
8 августа 2021 в 21:57
3

Постарайтесь сделать фрагмент вашего примера минимальным; перечисление должно иметь только несколько членов, чтобы вы могли продемонстрировать поведение.

Adrian Mole
8 августа 2021 в 22:11
1

Не могу воспроизвести. Какой компилятор? Какой стандарт? Как выглядит ваш файл исходный (CPP)?

rturrado
8 августа 2021 в 22:11
1

Можете ли вы попробовать определить статический массив в файле cpp, а не в заголовке? Как предлагается здесь: coderhelper.com/a/2117331/260313

Joe
8 августа 2021 в 22:16
0

Есть ли какая-то особая причина, по которой вы создаете член enum в списке инициализаторов конструктора с фигурными скобками вместо круглых скобок? Другими словами, зачем делать это Territory(Identifier identifier) : identifier{identifier} {} вместо этого? Territory(Identifier identifier) : identifier(identifier) {}

Ted Lyngmo
8 августа 2021 в 22:27
0

@Joe Почему бы не использовать синтаксис инициализации скобок?

dboeger1
8 августа 2021 в 22:29
1

Я пытаюсь обновить свой пример, чтобы сделать его немного проще, но имейте в виду, что я слежу за книгой и все еще довольно рано, поэтому я еще не очень хорошо знаком с тем, как правильно реорганизовать весь этот код. Чтобы ответить @Joe, книга, которую я читаю, называется «Ускоренный курс C++: быстрое введение» Джоша Лоспиносо, и в ней рекомендуется всегда использовать фигурную инициализацию. Я забыл точные примеры, но он предоставил несколько примеров пограничных случаев, когда круглые скобки работали не совсем правильно или были немного неинтуитивными. Для начала я пытаюсь выработать хорошие привычки, поэтому брекеты мне показались подходящим вариантом.

463035818_is_not_a_number
8 августа 2021 в 22:30
0

пожалуйста, прочитайте о минимально воспроизводимом примере. 2 или 3 значения перечисления было бы достаточно, чтобы продемонстрировать проблему

pelya
8 августа 2021 в 22:38
0

Этот код отлично компилируется: onlinegdb.com/Qeoqpoemm

JaMiT
8 августа 2021 в 23:54
0

Ваша ошибка, по-видимому, вызвана компиляцией clang для стандартов C++ 14 или более ранних версий. Для этих стандартов gcc выдает другое, но похожее сообщение об ошибке. При компиляции для C++17 ошибок не возникает. На какой стандарт вы собираетесь ориентироваться?

dboeger1
9 августа 2021 в 00:00
0

@JaMiT Это кажется вероятным, поскольку кто-то другой смог скомпилировать без проблем. Я намерен ориентироваться на последний стандарт (С++ 20, если я не ошибаюсь), если только нет веских причин не делать этого. На данный момент я просто слежу за книгой в учебных целях, поэтому не уверен, почему я буду нажимать что-то слишком новое синтаксически. Теперь, если бы я только мог понять, где, черт возьми, настройки компилятора находятся в Xcode...

JaMiT
9 августа 2021 в 00:02
0

@dboeger1 Помогает ли многие вопросы о различных компиляторах C++, доступных мне в OS X? Ему несколько лет, поэтому расположение опций могло измениться.

Ответы (1)

avatar
dboeger1
9 августа 2021 в 00:13
1

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

После некоторого обсуждения в комментариях и того, что кто-то смог собрать и запустить код без проблем, я покопался в настройках своего проекта Xcode и обнаружил, что он компилируется для C++14. Я изменил это на C++17, раскомментировал все 4 строки, и большая часть ошибок исчезла.

В моей строке цикла for в main.cpp осталась 1 ошибка:

Call to deleted constructor of 'TerritoryUSA::Territory'

Он также выделяет незакомментированную строку 1 серым цветом с этим сообщением:

1. 'Territory' has been explicitly marked deleted here

Эта строка является конструктором копирования, поэтому я предположил, что цикл for на основе диапазона был реализован с использованием конструкторов копирования. Я быстро добавил к строке модификатор ссылки, чтобы она выглядела так:

    for (Territory& territory : territories) {

и вуаля, он снова собирается и запускается. Я даже не думаю, что циклы for на основе диапазона существовали в C++, когда я использовал язык более регулярно, поэтому я не знал, будет ли модификатор ссылки работать там, как предполагалось, но, похоже, это так. Довольно изящно, почти как Python более низкого уровня без копирования.

Joe
9 августа 2021 в 00:16
0

На самом деле это не конструктор копирования. По крайней мере, не тот, который вы хотите. аргумент должен быть const Territory&, а не Territory&. Но рад, что вы решили свою проблему

dboeger1
9 августа 2021 в 00:21
0

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

Joe
9 августа 2021 в 00:26
0

Это довольно просто. Если функция, которую вы вызываете (конструктор копирования, в данном случае), должна не изменять переданный аргумент, тогда она должна быть константной. Конструктору копирования определенно не должно быть позволено изменять то, что дано. Он копирует его. Поскольку вы передаете объект Territory по ссылке, это означает, что конструктор имеет доступ к исходному базовому объекту. Это похоже на передачу указателя. Поэтому, если вы хотите сигнализировать всем звонящим, что он не будет изменен, отметьте его const Territory&.

Joe
9 августа 2021 в 00:29
0

Теперь, если бы территория была действительно тривиальным классом с семантикой, позволяющей копирование/назначение (чего, как я понимаю, нет). Тогда у вас может быть конструктор копирования, который принимает аргумент только Territory (т.е. передает по значению, которое неявно создает копию), а не const Territory&. Но вы явно пытались запретить это, так что const Territory& это так.

Joe
9 августа 2021 в 00:39
0

Я должен добавить, что я начал работать с C++ еще в 1990-х годах, задолго до того, как появились такие вещи, как ссылки на R-значения. Вот почему я спросил о фигурных скобках вместо скобок для списка конструкторов-инициализаторов. Мое представление о «лучших практиках» иногда опровергается улучшениями языка. (Поэтому мне нужно будет прочитать, почему эта практика лучше...) Но все, что я написал в двух комментариях выше, по-прежнему технически верно.