Нет доступных конструкторов копии или конструктор копий объявлен как explicit

Обновлено: 28.03.2024

Здравствуйте, watchmaker, Вы писали:

W>Здравствуйте, andyp, Вы писали:

A>>То, что у тебя названо конструктором копирования T3DVector таковым не является.
W>Как раз является.

A>>Сигнатура должна быть T3DVector(const T3DVector&)
W>Не должна быть, а может быть такой.

Формально ты прав. Но ведь все поняли, что я имел в виду Ну как-то язык не поворачивается назвать конструктором копирования конструктор, который может менять источник.

Здравствуйте, andyp, Вы писали:


A>Но ведь все поняли, что я имел в виду :)
Это главное.

A>Ну как-то язык не поворачивается назвать конструктором копирования конструктор, который может менять источник.
Казалось бы, умные указатели — широко распространённый пример такого поведения. Например, простой и яркий представитель ­linked_ptr явно при копировании изменяет связи внутри исходного объекта, добавляя себя в список владельцев. Ну и другие сценарии с подсчётом ссылок или copy-on-write оптимизациями также могут модифицировать источник.
Хотя, надо признать, что даже в этом случае сигнатура обычно содержит константную ссылку, а сама модификация разрешается через mutable члены класса. Может быть из-за того, что mutable не видно в сигнатуре, и создаётся впечатление, что копирование никогда не модифицирует исходный объект? :)

Здравствуйте, watchmaker, Вы писали:

W>Может быть из-за того, что mutable не видно в сигнатуре, и создаётся впечатление, что копирование никогда не модифицирует исходный объект?

/философия on
Дык. Value semantics же. Копирование имеет смысл для объектов, у которых эта самая value semantics есть. Иначе как можно получить копию чего-либо? Объекты, про которые ты писал, они как-бы не совсем такие, но изо всех сил стараются такими быть Отсюда и mutable члены и const в конструкторе копирования. Притворяшки, вобщем.
/философия off

RF>>error C2558: class 'T3DVector': нет доступных конструкторов копии или конструктор копии объявлен как 'explicit'

A>Прикольно, компилятор на русском пишет.

RussianFellow:

A>>Напиши итератор, который прибавляет к адресу 10 при вызове operator++() и скорми его в inner_product.
RF>andyp, не могли бы Вы дать ссылку на материал, где подробно рассказывается об inner_product ?
A>>PS vector для 2мерной матрицы делать лучше не надо. Сделай vector размером (сроки*столбцы)
RF>Почему? Ведь, как я понимаю, можно пользоваться и vector .

Вектор векторов плохо потому, что происходит т.н. "глубокое копирование".
Т.е. при модификации элемента внешнего вектора, он будет заново пересоздан.
Это очень неэффективно. Помню, году в 2000 изобрател трехмерный велосипед с vector>>.
Работало на тогдашнем железе реально долгите минуты. А как оптимизировал — залетало махом

Если нужна именно матрица, чтобы реализовать двойной оператор [] можно сделать так:
Данные в матрице физически хранятся в одномерном векторе, и кроме того, хранится ширина строки.
operator [] матрицы возвращает некий дружественный матрице объект slice (срез), состоящий из ссылки на матрицу и номера строки. operator [] слайса уже возвращает настоящий элемент внутреннего вектора матрицы.
как-то так. Альтернативный, более опасный метод — operator [] матрицы возвращает указатель на элемент начала строки.
К которому можно применить встроенный оператор []. Недостаток — правый оператор [] будет без проверки диапазона.

Если структура данных не матрица — подвектора могут иметь разную длину, внутренний вектор можно завернуть в shared_ptr.
Вместо копирования всего подвектора будет копирование умного указателя на него.
Расходы на обращение к shared_ptr гораздо меньше чем к глубокому копированию.
Как-то так.

ЗЫ. На vector> можно можно делать рекурсивные структуры — деревья. Во.

Здравствуйте, Дрободан Фрилич, Вы писали:

ДФ>Вектор векторов плохо потому, что происходит т.н. "глубокое копирование".
ДФ>Т.е. при модификации элемента внешнего вектора, он будет заново пересоздан.

ты может с какими-нибудь строками путаешь, а std::vector в C++ всегда был mutable. "элемент внешнего вектора" — это внутренний вектор, так что вообще непонятно что ты под его модификацией понимаешь

ДФ>Это очень неэффективно. Помню, году в 2000 изобрател трехмерный велосипед с vector>>.
ДФ>Работало на тогдашнем железе реально долгите минуты. А как оптимизировал — залетало махом

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

Имеется класс Koords и конструктор копирования для него:

При этом если объявляю его как explicit , то перестает работать возврат Koords в функции, типа:

компилятор пишет, что нет подходящего конструктора.. Но какого? Конструктор копирования есть и у него есть Koords & в первом аргументе, так что вроде никаких преобразований и быть не должно. Но мне компилятор говорит обратное.

@alexolut вверху смотрите. Или пример для вас недостаточно минимальный? Или у вас какие-то проблемы, чтобы его воспроизвести?

Ага, проблема. В fun у вас используется конструктор по умолчанию, но его наличие в коде не указано. Т.е. пример не полный. Идеальный пример, когда код можно просто ctrl+c, ctrl+v и посмотреть на результат. Не додумывая ничего и не склеивая куски из разных частей.

2 ответа 2

В вашем случае подходящего конструктора копирования у вас не уже хотя бы потому, что первый параметр вашего конструктора копирования объявлен как неконстантная lvalue ссылка. Такую ссылку невозможно привязать к временному объекту Koords<> . Поэтому до С++17 (см. ниже) работать это не может в принципе, и explicit тут ни при чем вообще.

Что же касается explicit на конструкторе копирования. Конструктор копирования по определению всегда являлся частным случаем конструктора конверсии. Объявление конструктора копирования как explicit производит на него тот же самый эффект, как и на любой другой "одноаргументный" конструктор. Такой explicit конструктор будет использоваться только в контекстах, соответствующих прямой инициализации (direct-initialization), и не будет использоваться в контекстах, соответствующих копирующей инициализации (copy-initialization)

Возвращение значения из функции через return делается по правилам копирующей инициализации. Поэтому explicit конструктор копирования там использоваться не может

Обратите внимание, что в С++14 ошибочным по той же причине являются и такие варианты

но начиная с С++17 именно эти варианты уже являются корректными из-за guaranteed copy elision.


Предположим, что в программе на C++ вы возвращаете из функции локальную переменную. Что происходит при вызове оператора return : копирование, перемещение или ни то, ни другое? От этого зависит длительность вызова функции и эффективность наших программ. Я постарался разобраться с этим вопросом и дам рекомендации по написанию функций так, чтобы повысить шансы на применение этой оптимизации компиляторами. Ну, а сокращения в названии статьи — это Return Value Optimization (RVO) и Named Return Value Optimization (NRVO).

Определение NRVO и RVO

Давайте договоримся о терминах. Предположим, мы написали функцию:

где C — некий пользовательский класс. Что произойдет при её вызове?

Кажется, должна выполниться такая последовательность действий:

создание local_variable при помощи вызова конструктора по умолчанию класса C ;

вызов конструктора копии класса C , чтобы копировать local_variable в result ;

вызов деструктора для local_variable .

Действительно, это так и произойдёт, если не будет использована Named Return Value Optimization (NRVO). А если будет, то вместо создания local_variable компилятор сразу создаст result конструктором по умолчанию в точке вызова функции f() . А функция f() будет выполнять действия сразу с переменной result . То есть в этом случае не будет вызван ни конструктор копии, чтобы скопировать local_variable в result , ни деструктор local_variable . Можно представить это так:

компилятор создаёт конструктором по умолчанию до вызова функции f() переменную result ;

затем неявно передаёт в функцию f() указатель на result ;

в рамках функции f() не создаёт local_variable , а вместо этого работает с указателем на result ;

в return ничего не копируется, поскольку данные уже там.

Что же касается Return Value Optimization, то это такая же оптимизация, как NRVO, но для случаев, когда экземпляр возвращаемого класса создаётся прямо в операторе return :

В таких ситуациях компилятору легче выполнить оптимизацию, чем в случае с NRVO.

Обычно компилятор, даже со всеми включёнными оптимизациями, не обязан применять RVO/NRVO, а лишь имеет на это право. Поговорим о том, что мешает компилятору применять эти оптимизации, а что помогает, и о том, как повышать шансы на их применение.

Чтобы не загромождать статью, я разберу только два случая, упомянутых выше:

Локальная переменная возвращается из функции (NRVO).

Объект, созданный в точке вызова return , возвращается из функции (RVO).

Отмечу также, что стандарт и некоторые другие источники предпочитают вместо RVO/NRVO употреблять более общий термин copy elision (пропуск копии). Пару слов о нём скажу в конце статьи.

Случаи, когда компилятор обязан применить RVO

В C++17 есть два случая, когда компилятор обязан применить RVO.

1. Функция возвращает prvalue.

Во-первых, RVO будет применяться, когда возвращается prvalue того же типа, что описан в сигнатуре функции, или когда возвращается тип, из которого может быть сконструирован (явно или не явно) тот тип, который задан в сигнатуре. При этом игнорируются квалификаторы const и volatile . А операторов return может быть несколько, но все должны возвращать prvalue . Например, в этом случае будет всегда применено RVO:

Также всегда будет применено RVO в таком случае:

при условии, что у класса C есть конструктор от int , который не объявлен как explicit. Именно RVO, поскольку экземпляр класс C будет сконструирован неявно из n в операторе return .

До 17-го стандарта это было рекомендацией для компилятора, а в C++17 стало обязательной «оптимизацией». Я взял термин в кавычки, поскольку с точки зрения C++17 это уже не оптимизация, а обязательная часть работы компилятора. Более того, согласно новому стандарту, чтобы код выше скомпилировался классу C не требуются конструктор копии и перемещающий конструктор, поскольку эти конструкторы гарантированно не будут использованы при вызове f() . То есть в примере result будет создан сразу в точке вызова функции f() .

Конструктор копии и перемещающий конструктор могут отсутствовать только в том случае, если компилятор обязан применить RVO. Если компилятор не обязан применять RVO/NRVO, а лишь имеет на это право, то перемещающий конструктор или конструктор копии будет нужен для того, чтобы сработать, если оптимизация не получится.

2. Constant expression и constant initialization.

Также компилятор обязан применить RVO в функциях времени компиляции ( constexpr ) и при инициализации глобальных, статических и thread-local переменных (constant initialization). Интересно, что в этих же случаях применение NRVO гарантированно не случится. Рассмотрим на примерах:

Здесь при инициализации global_rvo гарантированно применится RVO. Строчка S global_rvo = rvo(); скомпилируется, даже если у структуры S не будет конструкторов копии или перемещения. А вот для инициализации global_nrvo необходимо, чтобы у структуры S были конструкторы копии или перемещения, поскольку один из них должен быть вызван в обязательном порядке. Ведь, как отмечено выше, в случае constant initialization NRVO применять нельзя.

Теперь поговорим про случаи, когда компилятор сам решает, применять ли ему RVO/NRVO.

Необходимые условия для применения RVO/NRVO

Необходимые условия для применения этой оптимизации:

Тип объекта, возвращаемого из функции согласно сигнатуре, должен совпадать с типом локального объекта или конструироваться неявно из типа локального объекта. Допустимо отличаться на константность.

Возвращаться должен именно локальный объект, а не ссылка на него или какая-то его часть.

В случае NRVO возвращаемый объект не должен быть volatile.

Поясню на простом примере:

Здесь NRVO может быть применено, поскольку N конструируется из p .

Случаи, когда компилятору сложно применить RVO/NRVO

Предположим, выполнены все необходимые условия. Тем не менее, обычно оптимизация не применяется в следующих случаях:

Есть несколько путей выхода из функции, которые возвращают разные локальные объекты.

Возвращаемый локальный объект ссылается на встроенный asm-блок.

Не стоит писать return std::move(local_value)

Рассмотрим пример из начала статьи:

В нём оптимизация NRVO, скорее всего, сработает. Допустим, мы уверены, что у класса C есть перемещающий конструктор. Тогда может возникнуть соблазн написать что-то вроде:

Так делать ни в коем случае нельзя, иначе компилятор не сможет применить NRVO. В таком случае в операторе return будет вызван перемещающий конструктор класса C . На его вызов требуются ресурсы. А если перемещающего конструктора нет, то будет вызван конструктор копирования. Поясню подробнее.

В случае, если необходимые условия для применения NRVO выполнены (пример без std::move() ), но оптимизация не применена по каким-то причинам, то возвращаемый объект обязан быть рассмотрен как rvalue . То есть возвращаемый объект будет рассмотрен так, как если бы к нему было применено std::move() . Выражаясь иначе, если NRVO не применено, то при наличии у возвращаемого объекта перемещающего конструктора будет вызван он. А если перемещающий конструктор отсутствует, то будет вызван конструктор копирования. И для этого не нужно писать std::move(local_variable) .

Эти рассуждения приводят нас к тому, что применение std::move() к возвращаемому локальному объекту не приносит никакой пользы. Более того, это вредит: std::move() меняет тип возвращаемого объекта. По сути, функция возвращает rvalue-ссылку на тип объекта, которые ей передаётся в качестве аргумента. То есть в нашем случае это будет rvalue-ссылка на локальный объект типа C . А, как отмечено выше, это препятствует применению NRVO, поскольку тип возвращаемого объекта будет rvalue-ссылка на тип C , в то время как f() возвращает просто тип C .

То есть при возвращении из функции локального объекта в операторе return не стоит писать std::move() . Однако нередко бывает оправдано применение std::move() к возвращаемому из функции параметру, если он передан по rvalue-ссылке. Но это тема для отдельного обсуждения.

Как включить и выключить RVO/NRVO-оптимизацию в компиляторах

Оптимизация RVO/NRVO по умолчанию включена. Отключить её можно при помощи флага компиляции -fno-elide-constructors . Важно отметить, что флаг отключает именно оптимизацию, то есть в случаях, когда стандарт гарантирует отсутствие копирования возвращаемого значения это копирование не происходит даже с этим флагом.

Проверка, сработает ли RVO/NRVO в конкретном случае

Иногда хочется узнать, сработает ли RVO/NRVO в конкретном случае. Сделать это очень просто: достаточно вставить в возвращаемый класс записи в лог в некоторые специальные методы, а именно в конструкторы, в перемещающий конструктор, в конструктор копии и деструктор. Вот пример класса, который можно вернуть из функции и по логам понять, сработает ли RVO/NRVO:

Другие случаи отмены копирования (copy elision)

В C++ копирование иногда отменяется и в других случаях. Например, в этом коде вызывается только конструктор по умолчанию для инициализации C:

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

И еще одно важное замечание. В C++ есть правило "as-if": компилятору разрешено не генерировать код для операций, если их отсутствие не изменит поведение программы, наблюдаемое со стороны. Например, конструктор копии, который компилятор генерирует автоматически, может быть не вызван, если это не изменит поведение программы. Но если мы вместо автоматически сгенерированного конструктора копии напишем свой конструктор копии, в котором выведем что-то в лог, то внешне наблюдаемое поведение изменится, потому что при его вызове что-то будет выведено в лог. И после этого он станет вызываться.

Так вот, в статье были описаны только случаи, когда компилятор обязан применить RVO или может применить RVO/NRVO несмотря на изменение внешне наблюдаемого поведения. То есть, допустим, мы в специальных методах (конструкторе по умолчанию, конструкторе копии, перемещающем конструкторе и т.д.) выводим информацию в std::cout , меняя внешне наблюдаемое поведение. Эти специальные методы компилятор всё равно сможет удалить при применении RVO/NRVO.

Вызов кода конструктора копии и перемещающего конструктора

Всё вышесказанное имеет очевидное следствие, на которое я хотел бы обратить внимание. В ряде случаев компилятор сам решает, применять ли RVO/NRVO. Если применяет, то конструктор копии или перемещения не будет вызван, а если не применяет — то будет. Это зависит от компилятора и платформы. Поэтому не стоит полагаться на вызов этого кода и размещать в нём что-то важное кроме копирования или перемещения объекта.

Выводы

RVO/NRVO не новые оптимизации. Компилятор имел право применять их ещё в стандарте C++98. По прошествии времени стандарт стал строже регламентировать применение этих оптимизаций. Однако по-прежнему остаётся довольно большая серая зона, в которой компилятор решает, применять ли RVO/NRVO. И если нет возможности гарантировать применение RVO, то стоит хотя бы постараться повысить шансы на её применение.

Рекомендации

Целесообразно прежде всего рассмотреть возможность вернуть prvalue , то есть создать экземпляр класса прямо в операторе return . Это будет гарантировать отсутствие копирований и перемещений, а также не потребует от создаваемого объекта конструкторов копии и перемещения. Даже если операторов return , в которых создаются объекты, будет несколько.

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

Также в случаях, когда мы рассчитываем на применения RVO/NRVO, важно следить за тем, чтобы тип создаваемого в операторе return объекта или тип локального объекта, возвращаемого из функции, точно совпадал с типом, прописанным в сигнатуре функции. Ну, или, как минимум, чтобы из типа, возвращаемого из функции, мог быть сконструирован тип, прописанный в сигнатуре функции.

Важно также следить за тем, чтобы в операторе return , возвращающем локальный объект, не стояло std::move() .

@Jesse J: И то, и другое в порядке. Эти два способа имеют немного разное поведение, которое во всех случаях, кроме самых дьявольских, дает один и тот же результат. Технически ваш метод создаст тестовый класс, а затем назначит его g, а не просто инициализирует g. Это становится проблемой только тогда, когда у вас есть настраиваемое поведение копирования / назначения / инициализации. - Akusete

Спасибо вам всем. Действительно информативные ответы. - Carl

4 ответы

В основном std::auto_ptr нельзя использовать таким образом.

Требуется конструктор копирования, который принимает const& существует, и такой конструктор не существует для std::auto_ptr . Это широко рассматривается как Хорошая Вещь с вы никогда не должны использовать std::auto_ptr в таре! Если вы не понимаете, почему это так, то прочитайте эту статью Херба Саттера, особенно раздел, озаглавленный "Вещи, которые не следует делать, и почему их не делать" примерно 3/4 пути.

ответ дан 06 авг.

Если бы стандартные контейнеры были обязаны использовать swap чтобы копировать вещи вокруг, auto_ptr будет работать. И мне очень жаль, что они были. В C ++ 0x, ::std::unique_ptr (что очень похоже на ::std::auto_ptr ) также не имеет конструктора копирования и имеет только конструктор перемещения, а стандартные контейнеры должны использовать конструктор перемещения для перемещения своего содержимого, поэтому вы можете сохранить ::std::unique_ptr в них и заставить его работать, как ожидалось. - Всевозможный

На самом деле использование auto_ptr в контейнере, потому что контейнеры STL требуют, чтобы их члены имели «нормальное» поведение копирования. Auto_ptr не соответствует этому требованию. - Билли Онил

@Omnifarious: вы смешиваете shared_ptr с unique_ptr. Shared_ptr - это умный указатель с подсчетом ссылок, а не с семантикой перемещения. - Билли Онил

@Billy - Придет ли полиция кода и заберет вас, если вы это сделаете, или это просто приведет к ошибке компилятора? Ой! И ты прав насчет shared_ptr . Я исправлю это сейчас. - Всевозможный

@Omnifarious: это предполагаемый быть ошибкой времени компиляции. Однако не все компиляторы это делают. Для компиляторов, где это не ошибка времени компиляции, это неопределенное поведение. Вы не смогли бы этого сделать, даже если бы у вас было что-то вроде swap основанные на внутренностях для этих контейнеров, потому что стандартные алгоритмы все равно все испортят (они ожидают, что смогут безнаказанно копировать) - Билли Онил

Вы пытаетесь подтолкнуть auto_ptr в std::vector

auto_ptr не определяют неявный конструктор копирования и несовместимы в качестве значения в классах контейнера stl.

Я был почти уверен, что ответ на этот вопрос был: «Никогда, никогда шаблон не может быть конструктором копирования».

К сожалению, я потратил 3 часа, выясняя, почему я получаю предупреждение о рекурсии, отслеживал его до конструктора копирования, наблюдал, как отладчик сходил с ума и не позволял мне смотреть на рекурсивный код, и, наконец, отследил его до отсутствующего '& 'в базовом конструкторе.

Видите ли, у меня есть этот комплексный узел проектирования на основе политик, который уже некоторое время работает нормально. Я переопределил две политики в одной и наткнулся на конструктор рекурсивного копирования. Сузил его до одной политики, которая требуется для предоставления конструктора, который может принимать тип концепции XXX в качестве аргумента, но в этом случае я просто отбрасываю ее. Я написал

Теперь my_policy является базовым классом для хоста (конечно), и эта небольшая опечатка вызвала рекурсию, когда конструктор копирования хоста передавал себя по цепочке этому шаблонному конструктору, а не неявному конструктору копирования, сгенерированному компилятором. Затем он, конечно, снова вызовет свой конструктор копирования, чтобы создать временный.

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

Я использую библиотеку параметров boost, чтобы сделать 7 или около того аргументов хосту доступными по «имени». Может быть, в этом проблема, я не знаю. Во всяком случае, мне интересно, знает ли кто-нибудь, какие конкретные условия, если таковые имеются, могут заставить компилятор законно использовать шаблонный конструктор для «base» в качестве конструктора копирования или из конструктора неявного копирования для «производного».

Редактировать примечание:

Я воссоздал проблему в приведенном выше коде, указав "another_base" явный конструктор копирования:

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

Больше информации:

Посмотрев на ответ Шауба, я взял приведенный выше код и скомпилировал его. Он отлично компилируется, пока вы не раскомментируете объявление конструктора копирования base2. Затем он взорвется так, как я предполагаю, с исходным кодом (без доступа к частному конструктору в базе). Так что шаблоны даже не являются частью проблемы; вы можете воссоздать проблему без них. Похоже, это проблема MI, и VS всегда немного медленно исправляла ее.

Читайте также: