Можно ли сделать exception-safe T Stack::pop()? Например, так:
template <class T> class Stack {
T Pop();
private:
T* v_; // ptr to a memory area big
size_t vsize_; // enough for 'vsize_' T's
size_t vused_; // # of T's actually in use
};
template<class T>
T Stack<T>::Pop()
{
if( vused_ == 0)
throw"pop from empty stack";
T result = v_[vused_-1];
--vused_;
try {
return result;
} catch (...) {
++vused_;
throw;
}
}
Здравствуйте, Marty, Вы писали:
M>Здравствуйте, T4r4sB, Вы писали:
NI>>>std::move_if_noexcept
TB>>Что это за срань? На С++ чтоб писать, это всё надо знать?
M>Ну, можешь писать на C с классами
А можно писать на С с шаблонами и деструкторами? Но без std::one_more_shitty_builtin?
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
TB>>>Что это за срань? На С++ чтоб писать, это всё надо знать?
M>>Ну, можешь писать на C с классами
TB>А можно писать на С с шаблонами и деструкторами? Но без std::one_more_shitty_builtin?
Можно. Пиши
ЗЫ На самом деле например эти всякие штуки из type_traits, которые появились в 11 стандарте, очень удобная штука — делать специализации для разных типов — интегральное/нет, целое/нет, ссылка/указатель/значений, и тп
Еще понравилось std::make_unsigned — параметр шаблона задавал размерность в битах, но могли ведь подсунуть и знаковый, что всё поломало бы. А так — из заданного типа вывел его unsigned аналог и всё.
Многое пока правда не понятно, многого не знаю, но в целом — имхо полезного много появилось
Здравствуйте, N. I., Вы писали:
NI>Максим Рогожин:
МР>>Можно ли сделать exception-safe T Stack::pop()?
NI>Можно.
А что тогда в статьях про Exception Safety пишут, что T Stack::pop() не может быть полностью exception safe и поэтому его разбили на два метода T top() и void pop()?
Максим Рогожин:
МР>А что тогда в статьях про Exception Safety пишут, что T Stack::pop() не может быть полностью exception safe
Когда эти статьи писались, получение результата и удаление элемента одной транзакцией именно в таком виде было нереализуемо, хотя в случае простых типов такое было возможно и в C++98.
МР>поэтому его разбили на два метода T top() и void pop()
Версия pop, которая просто удаляет элемент, всё равно нужна, чтоб была возможность гарантированного удаления без выброса исключений и без оверхеда от копирования — на случай, если значение удаляемого элемента не интересует.
Здравствуйте, T4r4sB, Вы писали:
NI>>std::move_if_noexcept
TB>Что это за срань? На С++ чтоб писать, это всё надо знать?
Все знать невозможно, коночно. Но можно попобовать добавить позитива в свои оценки. Я вот в последнее время прежде, чем мастерить что-нибудь на коленке, сначала смотрю, а нет ли этого уже в стандартной библиотеке. И нередко бывает: "Ух ты, а такая срань есть уже!". И так приятно сразу
NI>Когда эти статьи писались, получение результата и удаление элемента одной транзакцией именно в таком виде было нереализуемо, хотя в случае простых типов такое было возможно и в C++98.
что изменилось с тех пор? почему такое стало возможным?
зы. давай механизм move оставим за скобками, т.к. он лишь замыливает взгляд и сфокусируемся на типах T, которые копируются, но не муваются
uzhas:
U>что изменилось с тех пор? почему такое стало возможным?
В общем, оно и раньше было возможным при условии, что компилятор гарантированно делает copy elision в нужных местах: заводишь локальную переменную, копируешь в неё результат, вызываешь деструктор у топового элемента, уменьшаешь счётчик числа элементов на единицу — всего-то и делов. Если компилятор применяет NRVO, то возвращённый объект можно поюзать, не боясь его "потерять" из-за исключения при копировании.
Если же брать худший сценарий, когда NRVO нет, то деструктор придётся вызывать после инициализации return object, и всунуть такой вызов можно разве что в другой деструктор, который должен как-то определить, было ли брошено исключение. В C++17 наличие нового исключения можно обнаружить с помощью std::uncaught_exceptions, а в более ранних плюсах такой функции нету.
Здравствуйте, Marty, Вы писали:
M>ЗЫ На самом деле например эти всякие штуки из type_traits, которые появились в 11 стандарте, очень удобная штука — делать специализации для разных типов — интегральное/нет, целое/нет, ссылка/указатель/значений, и тп M>Многое пока правда не понятно, многого не знаю, но в целом — имхо полезного много появилось
Они ведь и появились что бы героически бороться с проблемами которые вызваны собственно самим C++
uzhas:
U>в общем, ничего не изменилось с тех пор и статья актуальна и по сей день
"Актуальность" там разве что теоретически-познавательная, т.к. чтобы реально наткнуться на описанные проблемы, надо ещё хорошенько постараться в поисках компилятора без достаточной поддержки copy elision или специфических ключей, которые copy elision отключают.
U>действительно считаешь, что именно эта функция сделал прорыв и позволила сделать безопасным T pop() ?
Ну, попробуй найти сценарий, при котором
auto top_value = stack.pop();
удалит из stack топовый элемент без успешной инициализации top_value.
Здравствуйте, N. I., Вы писали:
NI>Если же брать худший сценарий, когда NRVO нет, то деструктор придётся вызывать после инициализации return object, и всунуть такой вызов можно разве что в другой деструктор, который должен как-то определить, было ли брошено исключение. В C++17 наличие нового исключения можно обнаружить с помощью std::uncaught_exceptions, а в более ранних плюсах такой функции нету.
Не, ну если уж погружаться в паранойю, то можно возвращать из pop() умный указатель, например так: typedef std::vector<std::shared_ptr<T>> stack_body
Только это всё в целом бессмысленно. Сама по себе концепция exception-safe довольно бессмысленная...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Максим Рогожин:
МР>А тот код, который я привел (обернуть return в try-catch) не решает проблему?
Если push и pop делать канонично — с инициализацией (а не присваиванием) и уничтожением элемента соответственно, — то одними try и catch там не обойдёшься.
Кроме того, до C++11 было дововольно мало "безопасных" способов поюзать результат такого pop при бросающем конструкторе копирования (если рассматривать некий сферический компилятор в вакууме без каких-либо гарантий, помимо описанных в стандарте C++). Например, тут
T t = stack.pop();
результат можно было "потерять" из-за исключения при конструировании t. А вот тут
T const &t = stack.pop();
— из-за исключения при конструировании временного объекта, который компилятору позволялось создавать в контексте инициализации ссылки t (интересно, когда-либо вообще были такие компиляторы, которые этим правом пользовались?).
Оставалось разве что метод какой-нибудь вызвать
stack.pop().member_function();
или к отдельному полю доступ получить
variable = stack.pop().data_member;
Не шибко богатый выбор.
В C++11 правила инициализации ссылок уже более вменяемые — там можно гарантированно забайндить ссылку непосредственно на return object функции pop
auto &&t = stack.pop();
и дальше делать с этим объектом что хочешь.
C++17 в добавок к этому гарантирует, что в следующем случае
auto t = stack.pop();
переменная t и return object функции pop являются одним и тем же объектом.
Здравствуйте, N. I., Вы писали:
NI>Кроме того, до C++11 было дововольно мало "безопасных" способов поюзать результат такого pop при бросающем конструкторе копирования (если рассматривать некий сферический компилятор в вакууме без каких-либо гарантий, помимо описанных в стандарте C++).
ты хочешь сказать, что из-за того, что в свежем стандарте появилось больше безопасных вариантов вызова метода T pop() проблема как бы исчезла?
она не исчезла, т.к. "опасные" варианты всё еще остались. в стандарте нет таких примеров, что вот вызывайте std::vector::at (для примера) только в одну строчку с присваиванием, иначе мы не даем гарантии на безопасность к исключениям. это ерунда какая-то, слишком уж много нюансов надо держать в голове
uzhas:
U>ты хочешь сказать, что из-за того, что в свежем стандарте появилось больше безопасных вариантов вызова метода T pop() проблема как бы исчезла? U>она не исчезла, т.к. "опасные" варианты всё еще остались.
"Опасные" варианты использования всегда можно придумать, в том числе и для пары top и pop:
T t = std::move(stack.top());
POTENTIALLY_THROWING_EXPRESSION;
use_value(t);
stack.pop();
Если POTENTIALLY_THROWING_EXPRESSION кинет исключение, то на вершине стека, возможно, останется лежать мусорный объект, а полезное значение топового элемента может быть "утеряно".
U>в стандарте нет таких примеров, что вот вызывайте std::vector::at (для примера) только в одну строчку с присваиванием, иначе мы не даем гарантии на безопасность к исключениям.
Тебе возможные варианты кривого использования std::vector::at привести, что ли?
U>это ерунда какая-то, слишком уж много нюансов надо держать в голове
Мда, это ж какой мегамозг надо иметь, чтоб держать в голове два действия: перемещение/копирование элемента и его удаление... С таким rocket science далеко не каждый справится.
U>что ты скажешь про такие варианты вызова? U>
U>в каких случаях C++17 нас защищает, а в каких у нас уже будут проблемы при исключениях при копировании?
Если T — это кошерный тип с небросающим перемещающим конструктором и небросающим перемещающим оператором присваивания, то копирования T тут нигде не будет (внутренности f1 — f4 не рассматриваем). В этом случае возможностей C++11 уже достаточно.
В случае, если для возврата значения из pop используется потенциально бросающее копирование, C++17 даёт возможность сделать отложенный вызов деструктора для топового элемента при условии, что исключение брошено не было. Дальше при инициализации параметра t функции f1 C++17 обеспечивает guaranteed copy elision.
Здравствуйте, N. I., Вы писали:
NI>В случае, если для возврата значения из pop используется потенциально бросающее копирование, C++17 даёт возможность сделать отложенный вызов деструктора для топового элемента при условии, что исключение брошено не было.
это стало возможным только благодаря функции std::uncaught_exceptions, ничего другого полезного для данной задачи не появилось в C++17. собственно, об этом я и спрашивал ранее
хотя, стоит заметить, реализация относительно сложная из-за нестандартизованных SCOPE_*, но рабочая
NI>- из-за исключения при конструировании временного объекта, который компилятору позволялось создавать в контексте инициализации ссылки t (интересно, когда-либо вообще были такие компиляторы, которые этим правом пользовались?).
NI>В C++11 правила инициализации ссылок уже более вменяемые — там можно гарантированно забайндить ссылку непосредственно на return object функции pop
Почему нигде об этом не пишут? Как конкретно изменился wording инициализации ссылок?
σ:
NI>>- из-за исключения при конструировании временного объекта, который компилятору позволялось создавать в контексте инициализации ссылки t (интересно, когда-либо вообще были такие компиляторы, которые этим правом пользовались?).
NI>>В C++11 правила инициализации ссылок уже более вменяемые — там можно гарантированно забайндить ссылку непосредственно на return object функции pop
σ>Почему нигде об этом не пишут?
А ты везде проверил?
σ>Как конкретно изменился wording инициализации ссылок?
C++98/03 [dcl.init.ref] / 5:
....
— If the initializer expression is an rvalue, with T2 a class type, and “cv1 T1” is reference-compatible with “cv2 T2,” the reference is bound in one of the following ways (the choice is implementation-defined):
— The reference is bound to the object represented by the rvalue (see 3.10) or to a subobject within that object.
— A temporary of type “cv1 T2” [sic] is created, and a constructor is called to copy the entire rvalue object into the temporary. The reference is bound to the temporary or to a subobject within the temporary. [Footnote: Clearly, if the reference initialization being processed is one for the first argument of a copy constructor call, an implementation must eventually choose the first alternative (binding without copying) to avoid infinite recursion.]
C++11 [dcl.init.ref] / 5:
....
— If the initializer expression
— is an xvalue, class prvalue, array prvalue or function lvalue and “cv1 T1” is reference-compatible with “cv2 T2”, or
— has a class type (i.e., T2 is a class type), where T1 is not reference-related to T2, and can be implicitly converted to an xvalue, class prvalue, or function lvalue of type “cv3 T3”, where “cv1 T1” is reference-compatible with “cv3 T3”,
then the reference is bound to the value of the initializer expression in the first case and to the result of the conversion in the second case (or, in either case, to an appropriate base class subobject). ....
C++17 [dcl.init.ref] / 5.2.1:
— If the initializer expression
(5.2.1.1) — is an rvalue (but not a bit-field) or function lvalue and “cv1 T1” is reference-compatible with “cv2 T2”, or
(5.2.1.2) — has a class type (i.e., T2 is a class type), where T1 is not reference-related to T2, and can be converted to an rvalue or function lvalue of type “cv3 T3”, where “cv1 T1” is reference-compatible with “cv3 T3” (see 16.3.1.6),
then the value of the initializer expression in the first case and the result of the conversion in the second case is called the converted initializer. If the converted initializer is a prvalue, its type T4 is adjusted to type “cv1 T4” (7.5) and the temporary materialization conversion (7.4) is applied. In any case, the reference is bound to the resulting glvalue (or to an appropriate base class subobject).