Комментарии 83
class X
{
public:
X(const X&) = delete;
X& operator=(const X&) = delete;
X(X&& src) noexcept;
X& operator=(X&& src) noexcept;
// ...
};
Запрещать копирование не нужно, т.к. при наличии перемещающих конструктора и оператора присвоения компилятор не станет генерировать их копирующие аналоги.
Это как раз правило хорошего стиля: "Если определяешь или =delete любую операцию по умолчанию, то определи или =delete их всех".
Вообще, правило хорошего тона — это не использовать пользовательские конструкторы копирования и перемещения вообще. В этом случае компилятор их сгенерирует автоматически. Определение этих конструкторов явным образом нужно разве что при разработке библиотек базовых классов.
So as soon as any of the special functions is declared, the others should all be declared to avoid unwanted effects like turning all potential moves into more expensive copies, or making a class move-only.
Т.е. запрещайте, чтобы избежать неких «нежелательных эффектов». А никаких эффектов и без вмешательства кодера не будет, поведение компилятора в этом случае выглядит вполне разумным. Ну и традиционное, как пьяный дед мороз под новый год, «explicit is better than implicit»:
Defining only the move operations or only the copy operations would have the same effect here, but stating the intent explicitly for each special member makes it more obvious to the reader.
P.S. лично я предпочитаю не загаживать исходный код вещами, ничего не дающими ни кодеру, ни компилятору
Если бы это было мнение комитета по стандартизации, это было бы в стандарте.
Стандарт — не самое релевантное место для best practices.
А вообще, когда лет 10 назад обсуждалась семантика перемещения, было очень много обсуждений про то, как сделать поведение максимально явным и без сюрпризов, как при этом сохранить обратную совместимость и как сделать так, чтобы максимум кода получил профит.
В итоге имеем то, что имеем.
Или станет? А я точно правильно эти правила запомнил? А пришедший завтра мид про них помнит/знает?
Присоединяюсь к тем, кто считает, что их лучше писать явно.
Все это и так можно найти где угодно, и про RAII, и про move-rvalue. Не очень ясна цель поста — было б интереснее увидеть какие-то особенности или малоизвестные детали копирования/перемещения: например, RVO/NRVO, enable_shared_from_this, вопросы многопоточности shared_ptr (хотя такая статья точно здесь есть)
было б интереснее увидеть какие-то особенности или малоизвестные детали копирования/перемещения
Aliasing shared_ptr, хехе. Восьмой конструктор здесь.
Труд похвальный, но сам Страуструп уже над таким работает более масштабно: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#main
Не совсем вижу смысл в "конкуренции" без фактического предложения альтернативы. Может есть смысл заняться переводом?
x.DoIt(); // DoIt() &
X().DoIt(); // DoIt() &&
Не зря дочитал до конца. Спасибо!
1. В конструкторах и деструкторах нельзя (на самом деле можно, но в большинстве случаев это будет выстрелом в ногу) вызывать виртуальные методы. А иногда очень хочется это делать.
2. При конструировании объекта единственным способом сообщить о неудаче является исключение. А это не всегда удобный механизм (про производительность вообще молчу).
3. Из деструкторов вообще нельзя бросать исключения. Если что-то пошло не так, то программе придётся упасть, тогда как при неудачном вызове Destroy есть шанс отработать корректно.
4. Конструирование объекта может занимать длительное время, например, если объект — готовое сетевое соединение с базой данных. Если вы хотите передать код с синхронного на асинхронный, то в случае RAII вам придётся его полностью переписывать.
Резюме: если создание и разрушение объекта всегда производится без ошибок, а ошибка является фатальной, то используйте RAII. Если ошибка при создании объекта является штатной ситуацией, то RAII не нужен.
Ну и позволю себе чуток уточнить по вашим пунктам)
1. На самом деле нельзя) Если вы хотите вызвать из конструктора/деструктора виртуальный метод, то вы хотите странного.
2. Альтернатива — предусмотреть неинициализированное состояние объекта и перед использованием проверять, прям как в C. Исключение — удобный механизм, но производительность — соглашусь, хреновая. Но на то оно и исключение, что это именно что исключительная ситуация. А кинете вы исключение из конструктора или из фабрики — не суть важно, в любом случае все недоинициализированное придется так или иначе подчистить.
3. Полностью согласен, если Destroy вызывается не из деструктора какого-нибудь объекта в каком-нибудь фреймворке)
4. Если объект — готовое сетевое соединение, то он собственно уже готов, его осталось обернуть в RAII. Переделать код с синхронного на асинхронный — это все равно все к чертям переписать, и RAII там тоже нужен, но по-другому)
Как-то так)
1. На самом деле нельзя) Если вы хотите вызвать из конструктора/деструктора виртуальный метод, то вы хотите странного.Оконная библиотека. У вас есть абстрактный класс, который умеет группировать объекты и его многочисленные потомки — диалоговые окна, тулбары и прочее. Вам в конструктуре было бы неплохо узнать — а какой, собственно, будет размер «внутренностей» у вашего окна (тулбар и обычный диалог тут устроены сильно по разному, как вы понимаете). В Turbo Vision для Turbo Pascal — это сделано естественным образом, через виртуальные функции. В Tubro Vision для С++ — там костыль. Можно выбрать из несколькоих — но это всё равно будет костыль.
Хуже того, ради того, чтобы предотвратить это «преступление» инициализация каждого объекта делает бессмысленные действия — независимо от того, нужно вам это или нет. Это в языке, который постулирует себя как «платите только за то, что используете», да.
Да и вообще этот запрет, якобы не позволяющий «сделать себе плохо», смотрится дико посреди языка, представляющего собой большую коллекцию ногострелов разных сортов и размеров.
Что же касается конструкторов-деструкторов, то там много разного бывает, но это к языку имеет мало отношения, это просто общая проблема: часто понять — что делать, когда произошла обшибка тяжело из-за специфики области применения. Тут RAII может как помогать, так и мешать.
Особенно тяжело, действительно, обрабатывать случаи, когда «закрытие ресурса» может сорваться и, после этого, это можно как-то купировать…
В с++ есть очень важная гарантия — методы класса не могут быть вызваны перед конструктором и после деструктора.
Нету таких гарантий. Вот вам пример.
В с++ есть очень важная гарантия — методы класса не могут быть вызваны перед конструктором и после деструктора.Во-первых все ваши гарантии выеденного яйца не стоят, если в программе есть UB (это вам уже показали). Я если вы всегда пишете без ошибок — то нафига вам какие-либо гарантии.
Во-вторых если хотите без UB — то это тоже бывает. Ну или вот так.
Как я уже сказал — ногострелов в C++ более, чем достаточно, для того, чтобы можно было попадать себе в ногу многими интересными способами.
Виртуальные методы в конструкторе/деструкторе родителя эту гарантию нарушает.А также их нарушает куча других конструкций. И? Почему мы вот один конкретный способ стерльбы по ногам закрыли (причём закрыли путём усложенения компилятора и рантайма, а не просто как-нибудь), и при этом кучу других методов — оставили?
И я, например, хочу эту гарантию гораздо больше, чем решить вашу конкретную проблему чуть более элегантным (как вам кажется) способом.Он не самый элегантный. Он самый естественный. И во всех языках, которые не прилагали специалиных усилий (C#/Java, Object Pascal, далее везде) он работает. А вот в C++ — его закрыли. Ради каких-то странных гарантий, которые всё равно можно нарушить!
Вот в каком-нибудь языке типа Java (без Undefined Behavior, шаг влево, шаг право — попытка к бегству) это смотрелось бы допустимо. Но вот конкретно в C++ — это смотрится, по меньше мере, уродливо.
Однако вы не согласны, что это некие вырожденные примеры? А вот например ловить ошибку того, что я использую в своей функции переменную, у которой не сработал конструктор — гораздо больший гемор. Причем смотреть нужно на все переменные нашего класса во всех виртуальных функциях.
Кстати, вы похоже смотрели на другие языки. А как там решают данную проблему?
Однако вы не согласны, что это некие вырожденные примеры?Да, конечно. Но вырожденными они являются только потому что виртуальные функции там вызывать всё равно нельзя.
А вот например ловить ошибку того, что я использую в своей функции переменную, у которой не сработал конструктор — гораздо больший гемор.Не больший гемор, чем когда вы используете default initialization и обнаруживаете, что у вас в переменной мусор. MSAN точно так же смог бы это отловить.
Практика же использования других языков показывает, что с этой проблемой люди сталкиваются нечасто, уж по крайней мере недостаточно часто для того, чтобы на ровном месте порождать в языке, живущим под девизом «вы платите только за то, что используете» замедление «на ровном месте».
Кстати, вы похоже смотрели на другие языки. А как там решают данную проблему?Никак не решают. Потому что это — не проблема. «Конечный» конструктор (не вызванный из других) прописывает
vtable
— и всё, объект «готов к труду и обороне». До того, как сработает первая строчка пользовательского кода. В Java для безопасности все остальные поля обнуляются.«Конечный» конструктор (не вызванный из других) прописывает vtable
А как конструктор узнает, что он конечный?
Скорее указатель на vtable прописывает вызывающий код
::= C1 # complete object constructor
::= C2 # base object constructor
::= C3 # complete object allocating constructor
Может и вызывающий код прописывать — это от реализации зависит.
Главное, что текущее поведение — это в чистом виде усложнение и замедление. Дающее весьма мало преимуществ. Я, во-всяком случае ни разу не видел применения этой «фичи» на практике.
А от невозможности использовать виртуальные функции в конструкторе страдал часто.
При конструировании Base вызывается foo, которая в свою очередь (если бы вызывалась foo из Derived) вызывает функцию класса A. При конструктор A ещё не был вызван. И как бы вы ловили такое? Тем более что вызов foo вполне мог бы появится внезапно для автора Derived.
struct A{
A(){
// some important work
}
void do_work(){
// need internal initialized vars
}
};
struct Base{
Base(){
foo();
}
virtual void foo() { }
};
struct Derived : Base{
Derived() = default;
virtual void foo() override {
a.do_work();
}
private:
A a;
};
int main(){
Derived derived;
}
UB там тоже не обязательно, надо отметить.
operator->
, а разименовывать nullptr
нельзя. Если operator->
то там другие правила, но он, в свою очередь, не может быть статическим, так что UB случится всё равно.Как обычно — что и как сделает программа встретившись с UB никто не знает.
Так они и не будут там вызваны. Если у вас есть struct Derived : Base
, то пока выполняется конструктор Base
, объект ведёт себя как Base
, включая вызов виртуальных функций не ниже по иерархии, чем Base
.
То есть, вызывать вы можете всё что угодно (включая чистые вирутальные функции), просто они не дёрнутся из Derived
(что для чистых виртуальных немного больно).
Вам в конструктуре было бы неплохо узнать — а какой, собственно, будет размер «внутренностей» у вашего окна
я может быть туплю, но мне показалось, что вы хотите вызвать в конструкторе виртуальные методы объектов-агрегатов. В этом случае — никаких ограничений. А вот вызов виртуального метода самого объекта из конструктора противоречит просто здравому смыслу, так как предполагает выполнение кода потомка до вызова его конструктора
2. В таком случае все равно придётся выделять память под объект, плюс непонятно, как передавать код ошибки. Одним из полей объекта — неправильно. Пара (ссылка на объект, код ошибки), возвращаемая фабрикой, будет более логичным решением.
4. Я имел в виду, что на входе — параметры подключения, на выходе — установленное соединение, т.е. логика создания объекта очень сложная. В том же C# для переписывания синхронного кода в асинхронный требуется минимум телодвижений. Даже автоматические средства существуют.
1. Язык такую возможность позволяет — значит, можно. Пример: виртуальный метод tostring() и логирование, которое этот метод использует. Хочется использовать логирование и в конструкторе, и в деструкторе.Вот как раз логирование приводится как пример того, для чего всё это безумие нужно. Ибо так, как это реализовано в C++ — внутри конструктора A вы можете использовать логирование, но оно ничего не будет знать о том, что этому объекту суждено в будущем стать объектом B. И в деструкторе — тоже. вот так, типа безопасно.
Вот только на практике мне этим не пришлось воспользоваться ни разу, а вот с ошибками из-за «странного» поведения виртуальных функций — я регулярно сталкиваюсь.
2. Пара (ссылка на объект, код ошибки), возвращаемая фабрикой, будет более логичным решением.А что будет с объектом, которые не до конца сконструирован? Вызывать для него деструктор или нет? А если что-то удалось, что-то нет?
Это придётся очень сильно язык менять. Сейчас все эти краевые случае решаются как частный случай обработки исключения.
А что будет с объектом, которые не до конца сконструирован? Вызывать для него деструктор или нет? А если что-то удалось, что-то нет?
Предполагается, что объект либо конструируется полностью, либо не создаётся вообще. Очистка ресурсов в случае ошибки создания объекта — забота фабрики.
Нет, я верю что это всё можно разрулить. Но это приведёт к созданию сильно другого языка — мало похожего на современный C++.
C++ — внутри конструктора A вы можете использовать логирование, но оно ничего не будет знать о том, что этому объекту суждено в будущем стать объектом B. И в деструкторе — тоже. вот так, типа безопасно.
А как иначе то? Возьмем пример. Если бы A знал что ему суждено стать и перестать быть B, в A::foo будет UB — обращение к неинициализированному/удаленному объекту. А теперь представьте, что вы хотите отнаследоваться от класса, который в виртуальном деструкторе зовет несколько виртуальных же методов. Может быть всё-таки лучше как есть?
Может быть всё-таки лучше как есть?Как есть — это как? Примерно так: берём простенькую программу, делаем небольшой рефакторинг… трах, бах, расчленёнка, кишки наружу.
Великолепный подход, ящитаю.
Ну не работает эта попытка в коллекцию ногострелов привнести одну подушечку. Либо все стены должны быть мягкими, потому что в них головой будут биться (см. Java), либо уж сделайте так, чтобы было понятно как это работает — а программисты разбирутся.
Подход C++ успешно сочетает недостатки обоих подходов.
Пара (ссылка на объект, код ошибки), возвращаемая фабрикой, будет более логичным решением.
У вас goрячка, примите тип-сумму.
В том же C# для переписывания синхронного кода в асинхронный требуется минимум телодвижений. Даже автоматические средства существуют.
вот это для меня — новость. Пойду гуглить) У нас (сейчас я на scala) хоть и параллелится и асинхронится все влет, но автоматических средств нема
github.com/tumtumtum/Shaolinq/wiki/AsyncRewriter
Альтернатива — предусмотреть неинициализированное состояние объекта и перед использованием проверять, прям как в C.
На самом деле альтернатива — класс Foo
вообще без публичных конструкторов (кроме, возможно, move), но со статической функцией типа
static Either<ErrorType, Foo> makeFoo(All, The, Args, You, Need);
По большому счёту хороший решений в C++ ровно два:
1. Смириться с исключениями и делать так, как разработчики предусмотрели (хотя всё равно неясно что делать с деструкторами).
2. Отказаться от конструкторов и деструкторов и иметь методы Init/Destroy.
close(2)
вернулся с ошибкой — и да, это реально происходит и да, это-таки ошибка которую аккуратно написанные программы, например, emacs
обрабатывают).Ваши действия?
Не делать with. Там же закрытие ресурса неявное, а оно у меня тут самое что ни на есть явное.
У меня нет хорошего решения на C++, которое бы не дало вам забыть вызвать Close
(или не забыть сделать обработку ошибок от самого with). У меня есть хорошее решение на языках с линейными типами (там всё очевидно), и у меня есть хорошее решение на языках с Rank-2 polymorphism (классический финт с тегированием фантомной forall-переменной).
Но я, правда, немного думал, надо ещё подумать.
Там же закрытие ресурса неявное, а оно у меня тут самое что ни на есть явное.Вопрос не в «явное/неявное». Вопрос в том, чтобы случайно не забыть закрыть и при этом обработать ошибки.
А то рассказы про то, что
finally
не нужен, потому что есть RAII — есть, а объяснений как решить с помощью этого RAII простейшую задачу — нет. Есть много громоздких и некрасивых решений, а хорошего — я не знаю.А как вам finally
гарантирует, что вы не забудете закрыть и обработать ошибку?
Если вы ожидаете ошибку при создании объекта, то это не исключительная ситуация.
В хаскеле-то я тоже исключения кидаю, error
там всякие, когда происходит адок уровня совсем нарушенных инвариантов или чего такого.
Если вы ожидаете ошибку при создании объекта, то это не исключительная ситуация.От языка зависит. В том же python итератор имеет один метод
next
и кидает исключение, если всё закончилось.Но ему можно: там на производительность забили большой и толстый болт, так что не проблема. В C++ хотелось бы обойтись как-нибудь.
Невозможность записать в файл на уровне веб-инсталлятора — невосстановимая ошибка, можно показать соответствующее сообщение и потом счастливо закрыться.
Не получилось записать в файл в функции записи в файл — сообщить об ошибке наверх.
Выше попробовали как-то выкрутиться из ситуации (повтор записи, предложить выбрать другой файл для записи и т.п.), не получилось — вывалить ошибку уровню выше.
Ещё выше подчистили ресурсы, зафлушили другие файлы, записали лог, отвалились.
На разных уровнях одна и та же ошибка может иметь различную семантику и стратегия восстановления после сбоя может быть разной.
А на уровне выше в тупом и нетребовательном к стабильности и отказоустойчивости приложении if (!IsRight (result)) throw error;
Так что отличается, эм, всем? :)
На мой взгляд, лучше иметь перегрузки с/без исключений. Просто потому, что исключения часто удобнее, но не всегда подходят
Не можешь обработать — сообщи наверх. А исключение это, код ошибки, хитрый код ошибки, монада или что-то ещё — дело десятое.
Описанный подход с Either обладает тем недостатком, что заставляет либо тащить наружу все возможные детали всех возможных ошибок, усложняя обработку ошибок, либо, наоборот, скрывать причины ошибок. Меняется реализация с новым набором ошибок — боль либо неизвестность.
Технически есть — если у вас нет подходящего catch вплоть до main, то раскрутка стека не обязательна, ЕМНИП.
Да и с Either никто не мешает делать fromRight и подобное. Но это надо делать явно.
Но явно упоминать дополнительные типы нижележащих функций вам в вашем коде не нужно, вы просто добавляете тип вашей ошибки в список типов.
Функция-член Swap() определяется обычно легко: необходимо последовательно применять к базам и членам операцию обмена состояниями, если они ее поддерживают, и std::swap() в противном случае.
Если уж вы говорите про ADL, то имеет смысл упомянуть о таком паттерне, как
using std::swap;
swap(a, b);
ADL сам выберет подходящую версию функции.
RVO применяется разработчиками компиляторов достаточно давно и в настоящее время зафиксирована в стандарте C++11.
Которая при этом всё равно требует наличия accessible конструктора копирования.
А в С++17 добавили обязательное RVO, и теперь так можно возвращать классы, которые нельзя ни копировать, ни move'ать.
Спасибо!
Как обстоят дела с переопределением владения(смена владельца) членами?
Семантика копирования и управление ресурсами в C++