Comments 103
class person {
person(std::string first_name, std::string last_name)
: first_name(std::move(first_name))
, last_name(std::move(last_name))
Здесь должны быть универсальные ссылки, чтобы это работало адекватно.
Собственно, уровень знаний аффвтора, аналогичен и в остальном.
P.S.Дочитал. Автор не знает С++ совсем. Пусть хотя бы quiz сдаст.
Можно пояснить, что значит "адекватно"? Вроде всё правильно, никакого бреда. Вот идея передавать владение объектом (строкой), используя ссылку (универсальную или нет), как раз звучит шизофренически.
Всегда, когда у вас что-то создаётся в одном модуле, а потом передаётся в другой — есть эта проблема. А вот если нет — тогда всё «схлопывается», как и должно.
Ну а дальше — уже нужно решать вопрос: насколько для вас важно, чтобы данные, которые поступили откуда-то «извне» нормально отработались.
Впрочем если вы уже начали использовать
std::string
— то, скорее всего, вы уже не считаете все такты и миллисекунды.Она может «звучать шизофренически» — но таки иногда даёт ощутимый выигрыш.
Я знаю. Одно другому не мешает. "Звучит шизофренически" — это просто указание на то, что более сложный вариант — это преждевременная оптимизация. (В противовес противоположному мнению, что более простой вариант — это необоснованная пессимизация.)
Ну синтаксис у автора действительно немного странный, хоть и рабочий. Обычно в подобных случаях используют константные ссылки в качестве аргументов, что позволяет обойтись без странных костылей с std::move().
Константные ссылки в подобных случаях использовали в C++98. Современный стандартный подход — именно как у автора. В отличие от старого варианта с константными ссылками, он адекватно отражает семантику (передачу владения строкой) и позволяет полностью избежать лишнего копирования. И что вы странного видите в использовании std::move
? Оно именно для подобных случаев и существует.
Эээ, простите, тут не происходит никакой «передачи владения».
Именно это здесь и происходит. Внешний код передаёт строку экземпляру класса. Строки вообще редко одалживаются, но почти всегда передаются во владение.
Здесь создаются временные копии всех аргументов
Здесь не создаётся вообще никаких копий.
если аргументы нужны не полностью, а только для того, чтобы получить из них какую-то часть их данных
Если аргументы нужны не полностью, то имеет место не передача владения, а одалживание, и в таком случае передача по ссылке будет оптимальным синтаксисом, отражающим подразумеваемую семантику.
#include <string>
#include <iostream>
class foo
{
public:
foo(std::string arg1, std::string arg2) : m1(std::move(arg1)), m2(std::move(arg2)) {}
std::string m1, m2;
};
int main()
{
std::string arg1("AAA");
std::string arg2("BBB");
foo f(arg1, arg2);
std::cout << arg1 << arg2 << std::endl;
std::cout << f.m1 << f.m2 << std::endl;
}
выведет:
AAABBB
AAABBB
Никакой «передачи владения» не произошло, при вызове конструктора создались временные копии аргументов, которые затем были свапнуты move-конструкторами std::string с полями класса. Исходные строки остались нетронуты.
«Передача владения» здесь и вправду звучит несколько неверно, потому что в C++ (и не только) под этим словами обычно понимают иное.
P.S. А, вы имеете в виду, что этот синтаксис дает выбор между foo(arg1, arg2) и foo(std::move(arg1), std::move(arg2)). Тогда согласен.
Передача владения и копирование — это вещи ортогональные. Вы сделали копии и передали f
во владение копии. Если вам по логике работы программы необходимо иметь две копии соответствующих строк, естественно, что одного копирования не избежать. Если же вам не нужны две копии — надо использовать std::move
:
foo f(std::move(arg1), std::move(arg2))`
Оптимален со всех точек зрения — это вряд ли, специализированные конструкторы (которые будут принимать lvalue или rvalue ссылки) не будут допускать лишних вызовов конструкторов для своих аргументов, а тут частенько будут 2 вызова, когда в реальности достаточно одного. Ну, зато меньше кода писать, это да. Как швейцарский нож — годится для всего, но одинаково плохо.
// Временный объект (одно копирование и одно перемещение)
person("Very long string...");
// Константный объект (одно копирование и одно перемещение)
const std::string str("Very long string...");
person(str);
// Не константный объект (одно копирование и одно перемещение)
std::string str("Very long string...");
person(str);
// Объект с принудительным перемещением (одно копирование и одно перемещение)
std::string str("Very long string...");
person(std::move(str));
Если вы намекаете на лишнее перемещение (действительно бывают случаи когда даже лишнее перемещение сильно влияет на общую производительность), то современные стандарты гарантируют отсутствие перемещения во всех этих случаях. Прошу заметить, что тогда это очень специализированная ситуация. Объекты сами по себе очень большие или очень маленькие, типа int(). Ну тогда мы не может себе позволить даже инициализировать объект нулями и не применим весь подход к конструкторам из статьи.
то современные стандарты гарантируют отсутствие перемещения во всех этих случаях
С чего вы взяли? Берем такой код:
#include <string>
#include <iostream>
class arg_t
{
public:
arg_t() {std::cout << "DEF CALLED" << std::endl;}
arg_t(const arg_t&) {std::cout << "CPY CALLED" << std::endl;}
arg_t(const arg_t&&) {std::cout << "MOV CALLED" << std::endl;}
};
class foo_t
{
public:
foo_t(arg_t arg1) : m1(std::move(arg1)) {}
arg_t m1;
};
int main()
{
arg_t arg;
foo_t foo(arg);
}
Он выведет следующее:
DEF CALLED
CPY CALLED
MOV CALLED
Имеем и копирование, и перемещение сразу. Если мы заменим конструктор foo_t на такой:
foo_t(const arg_t &arg1) : m1(arg1) {}
то получим следующее:
DEF CALLED
CPY CALLED
Лишний вызов конструктора убрался. Аналогично для случая перемещения — будет 2 строчки, вторая «MOV CALLED», а в случае со «швейцарским ножом» будет 3 строчки, два MOV то есть. Можете сами поиграться:
godbolt.org/z/n5eCXS
Copy elision в данном случае вам никак не поможет.
Ну да, в таком случае оптимизатору компилятора негде развернуться.
Ну это мягко говоря не так, в случаях, когда copy elision действительно можно делать, никакой std::cout компилятору не мешает:
#include <string>
#include <iostream>
class arg_t
{
public:
arg_t() {std::cout << "DEF CALLED" << std::endl;}
arg_t(const arg_t&) {std::cout << "CPY CALLED" << std::endl;}
arg_t(const arg_t&&) {std::cout << "MOV CALLED" << std::endl;}
};
class foo_t
{
public:
foo_t(arg_t arg1) : m1(std::move(arg1)) {}
arg_t m1;
};
arg_t f()
{
return arg_t();
}
int main()
{
foo_t foo(f());
}
выведет:
DEF CALLED
MOV CALLED
А что так тоже можно?
Ну а почему нет, в данном случае он ничего не делает с аргументом, а по правилам матчинга вполне подходит. Если смущает, можете убрать const — будет ровно то же самое :)
Что, будем реально делать восемь перегрузок, на все комбинации?
Специально для этого придумали std::forward
И как же вы его будете использовать?
Вы как-то очень странно отвечаете: спорите одновременно и со мной, и с моим оппонентом — и все одним и тем же примером.
godbolt.org/z/Ceqiwi
Оптимален со всех точек зрения — это вряд ли, специализированные конструкторы (которые будут принимать lvalue или rvalue ссылки) не будут допускать лишних вызовов конструкторов для своих аргументов, а тут частенько будут 2 вызова, когда в реальности достаточно одного. Ну, зато меньше кода писать, это да. Как швейцарский нож — годится для всего, но одинаково плохо.
лишний move — не такая уж большая беда, тем более при фиксированном типе аргумента, у которого быстрый мув (а-ля std::string).
Вы забываете одну офигенно важную вещь, порождаему именно таким способом передачи аргументов — полученный конструктор безопасен относительно исключений. Если передавать константные ссылки — мы не получим такой фичи)
Конструктор, принимающий константные ссылки, тоже можно написать вполне безопасным с точки зрения исключений.
Проиллюстрирую — вот такой код безопасен относительно исключений:
SomeClass& SomeClass::operator=(SomeClass that) noexcept
{
swap(that);
return *this;
}
небезопасный — класс может быть в любом состоянии в случае исключения, в том числе потерять свой инвариант. UB короче.
базовая безопасность — класс может поменять состояние, но инвариант сохраняется
строгая безопасность — класс откатывается в точности в то состояние, в котором он был до вызова метода который вызвал исключение.
В случае передачи по значению, вы автоматом обеспечиваете строгую безопасность, грубо говоря, откатываете транзакцию, и вам не нужно ни о чем думать.
но при аккуратном написании проблем там минимум.Вот для аккуратного написания, желательно, правильно обходиться с исключениями, следить за ресурсами, минимизировать «ручное» управление и использовать RAII везде.
Проблема возникает не тогда, когда исключение возникает в конструкторе. Проблема тогда, когда исключение возникает в списке инициализации, т.е. ДО входа в конструктор :) Передавая аргумент по значению и делая std::move я получаю и noexcept (что немаловажно, кстати) и безопасность относительно исключений (у меня все иницилизируется гарантированно БЕЗ исключений, потому что все Move-конструкторы noexcept). Забавно, что в статье как раз-таки ссылаются на "невразумительное" решение, которое существует в С++ для решения этой проблемы.
Откройте Core Guidelines, там это всё написано, кстати.
Проблема тогда, когда исключение возникает в списке инициализации, т.е. ДО входа в конструктор :)
Пфф, так вы просто в данном случае переносите проблему в другое место. Вместо того, чтобы отловить исключение, выброшенное конструктором копии аргумента, из списка инициализации своего конструктора хотя бы даже через такое «невразумительное решение» и там же на месте, скажем, написать об этом в лог, вы его все равно словите, но уже в неявном виде и где-то хрен пойми где в произвольном месте своего кода. Считаете, это большой плюс?
Это огромный плюс, потому что проблема отлавливается там, где она создается :)
Представьте себе, что вы передается const-ссылку на семь уровней ниже по стеку, чтобы где-то там проиницилизировался объект, создав копию значения переданного по ссылке. При большой иерархии объектов, такая ситуация совсем не редкость. Используя передачу по значению + std::move мы выявим проблему в самом начале, там где она и возникла!)
К тому же, подход с передачей по значению, внезапно, уменьшает количество копий.
Вот пример кода:
class X
{
public:
X(const std::string& val) : m_str(val){}
private:
std::string m_str;
}
int main(void)
{
X x("hello");
}
При вызове конструктора класса Х:
1) Создается временный объект (выделение памяти) строки и передается в конструктор
2) Временный объект копируется в m_str (выделение памяти)
3) Возврат из конструктора вызывает деструктор временного объекта (освобождение памяти).
Если бы конструктор принимал строку по значению:
class X
{
public:
X(std::string val) noexcept : m_str(std::move(val)) {}
private:
std::string m_str;
}
int main(void)
{
X x("hello");
}
1) Создается временный объект (выделение памяти).
2) Временный объект передает владение памятью в аргумент конструктора (пара swap-ов указателя и длины)
3) Аргумент конструктора передает владение в m_str (опять же просто пара swap-ов)
4) Возврат из конструктора и вызов деструктора временного объекта который "пуст" и ничем не владеет, поэтому ничего не надо освобождать.
В итоге имеем на одно выделение памяти меньше. Если еще учесть такую штуку как Copy Elision, то скорее всего этапы 2 и 3 схолпнутся в один.
Плюс noexcept, плюс безопасность по исключениям и т.д. и т.п.
Используя передачу по значению + std::move мы выявим проблему в самом начале, там где она и возникла!)
Сомнительное достоинство. Вы с таким же успехом можете словить эту проблему на семь уровней выше по стеку просто потому, что там, где она возникла, у вас не стоит try/catch, а стоит где-то гораздо выше. Такая ситуация тоже совсем не редкость. Сильно сомневаюсь, что у вас прямо каждое место, где может быть сконструирован объект, заботливо обернуто в try/catch.
К тому же, подход с передачей по значению, внезапно, уменьшает количество копий.
Не всегда, я это уже выше демонстрировал. Зачастую он приводит, наоборот, к излишним вызовам конструкторов, которых можно было избежать.
Сильно сомневаюсь, что у вас прямо каждое место, где может быть сконструирован объект, заботливо обернуто в try/catchИзвините, но то что вы описываете — это антипаттерн. В том-то и прелесть исключений, что их нужно ловить только там, где вы знаете что с ними делать или на границе модулей. Безопасность кода с точки зрения исключений это совсем про другое.
Ну так человек, которому я отвечаю, как раз и радуется, что он якобы поймает проблему "сразу там, где она возникла", а я выражаю вежливое сомнение, потому что скорее всего он в любом случае поймает проблему не там, где она возникла, а там, где у него catch выставлен. Где проблема произойдёт, и где он её поймает — это две большие разницы.
Сомнительное достоинство
Так кажется ровно до тех пор, пока ты имеешь доступ ко всему коду, который используешь. Когда у тебя либа валится с исключениями где-то там далеко-далеко и исходников этой либы у тебя нет, вспоминаешь о том, как хорошо, когда либа предоставляет noexcept интерфейс (естественно, при условии, что он действительно сделан правильно).
Как по мне, правилом хорошего тона считается падать как можно быстрее и как можно ближе к месту ошибки.
К тому же, если новый объект создается через new, то снова возникае ситуация, когда мы зазря выделяли память (ведь this должен указывать на уже выделенную память). После исключения в конструкторе копирования аргумента где-то глубоко на седьмом уровне стека, мы не сможем доконструировать объект и хоть память и будет освобождена, оператор new зря старался запрашивая у ОС кусок памяти.
Не всегда, я это уже выше демонстрировал. Зачастую он приводит, наоборот, к излишним вызовам конструкторов, которых можно было избежать.
Это очень дешевые вызовы в подавляющем большинстве случаев, минусы от которых вполне возможно будут компенсированы наличием noexcept.
В вашем примере, кстати, собственно никакой семантики передачи владения и нет. Объект arg
вполне себе существует после вызова конструктора foo_t
, а значит вполне себе семантика копирования тут. Объект foo_t
попросту не владеет ресурсом, а владеет его копией.
Мне кажется мой аргумент по части двойного выделения ресурсов более убедительный, т.к. частенько можно увидеть код вроде:
class Struct
{
public:
Struct(std::string v1, std::string v2)
: value1(v1)
, value2(v2)
{}
private:
std::string value1;
std::string value2;
};
class ParsedMessage
{
public:
/// ...Impl
std::string getValue1()
{
return m_msg.substr(0, 3);
}
std::string getValue2()
{
return m_msg.substr(3, 10);
}
private:
std::string m_msg;
};
Struct MessageToStruct( const ParsedMessage& msg )
{
return Struct{ msg.getValue1(), msg.Value2() };
}
А вот код, в котором созданный объект, после передачи в другой объект продолжает использоваться мне встречается довольно редко. Если такое происходит, то обычно здесь замешан std::shared_ptr, который, к слову, правильно передавать именно по значению.
В отличие от старого варианта с константными ссылками, он адекватно отражает семантику (передачу владения строкой) и позволяет полностью избежать лишнего копирования.Вот только не надо всё это переносить в настоящее. Он может полностью избежать копирования и это, часто, но не всегда, реально происходит.
Увы, но тут та же ситуация, что и с STL в C++98: от того момента, пока были изобретены аьстракции, которые, вроде как, чисто теоретически, ничего не должны были стоить до того момента, пока они реально перестали чего-либо стоить — прошло много лет. Тут — примерно та же история.
Иногда, действительно, для борьбы с существующими компиляторами приходится использовать ссылки. Ну тут нужно понимать — почему и зачем.
Полный правильный ответ находится в учебнике Мейерса «Эффективный и современный С++» Глава 8.1
Другое дело, что предложенный автором вариант, как указано у того же Мейерса, не всегда плохой.
И задача ТС стояла в другом — показать [надуманную] проблему со списками инициализации, а не эффективный код. Так что, вероятно, я немного перегнул — приношу извинения…
Пример, приведенный для С++, укуренный неверный бред
Здесь должны быть универсальные ссылки, чтобы это работало адекватно.
И что же в этом примере кода работает неадекватно? И почему этот пример "укуренный неверный бред"?
Собственно, уровень знаний аффвтора, аналогичен и в остальном.
Автор довольно известный, можете посмотреть его код.
https://github.com/matklad
Посмотрел. У «известного автора», выпустившегося 5 лет назад, целый 1(один) репозиторий на С++.
Впрочем молодые революционеры такие и есть — им плевать на индустриальный опыт поколений. Я уж молчу про очередную попытку рассказать, что везде все плохо, только в Расте [будет] хорошо…
В Crystal всё работает.
class Test
def initialize()
test
@val = 1
end
def test
puts @val
end
end
puts "ok"
Test.new()
Error in line 13: instantiating 'Test.class#new()'
instance variable '@val' of Test must be Int32, not Nil
Error: instance variable '@val' was used before it was initialized in one of the 'initialize' methods, rendering it nilable
Rerun with --error-trace to show a complete error trace.
class Test
def initialize(a : String)
@str = a
@val = 0
end
def initialize(a : Int)
@val = a
end
def test
puts @val
end
end
puts "ok"
t = Test.new("123")
t.test()
Error in line 7: this 'initialize' doesn't explicitly initialize instance variable '@str' of Test, rendering it nilable
The instance variable '@str' is initialized in other 'initialize' methods,
and by not initializing it here it's not clear if the variable is supposed
to be nilable or if this is a mistake.
To fix this error, either assign nil to it here:
@str = nil
Or declare it as nilable outside at the type level:
@str : (String)?
Именно в такой постановке вопроса — нет, не интересен. Наличие конструкторов — не самоцель.
А чем это так принципиально отличается от подхода Rust, кроме того, что null в целом из основной части языка никуда не делся, и фактически все nullable-значения, судя по этому куску кода, ведут себя как Option?
if Random.rand > 0.5
test = "text"
else
test = 5
end
puts test
extern crate rand;
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let mut v;
if rng.gen_range(0, 9) > 5 {
v = 1;
} else {
v = "1";
}
println!("{}", v);
}
Именно по этой причине систем типов rust не может дать возможность реализовать безопасный конструктор, а не из-за того что это невозможно. То ли разработчики rust не хотели/не знали о таком, то ли решили что в системном языке это лишнее.
и фактически все nullable-значения, судя по этому куску кода, ведут себя как Option?Нет. Crystal может самостоятельно вывести алгебраические типы данных. String — гарантированно стока, String | Nil — аналог Option, String | Int из последнего примера — аналог enum из rust, самостоятельно выведенный компилятором. Он гарантирует, что не содержит Nil
play.crystal-lang.org/#/r/7a17 — ошибка выведения типа,
если же test += 1 — то тоже ошибка…
if Random.rand > 0.5
test = "text"
else
test = 5
end
if test.is_a? String
test += "1"
else
test += 1
end
puts test
Если же что-то можно сделать со всеми типами, то определять тип не требуется
if Random.rand > 0.5
test = "AЯ"
else
test = [0, 1]
end
puts test.size
То ли разработчики rust не хотели/не знали о таком, то ли решили что в системном языке это лишнее.В этом месте rust несколько… эээ… шизофренистичен. С одной стороны он не поддерживает автоматического создания «сложных» типов, которые не ложатся однозначно «на железо»… с другой стороны — он не даёт жёсткого описания того, как типы, которые в нём таки есть, на него ложатся.
Было бы разумно сделать выбор либо в одну сторону, либо в другую… но пока — вот так.
Именно по этой причине систем типов rust не может дать возможность реализовать безопасный конструктор, а не из-за того что это невозможно. То ли разработчики rust не хотели/не знали о таком, то ли решили что в системном языке это лишнее.
Ну это же не так. Если есть поддержка АДТ, то String | Int можно описать Either<String, Int>, и конструктор будет ничуть не менее безопасным, чем в Crystal. Так что не надо наезжать на пустом месте.
Если есть поддержка АДТ, то String | Int можно описать Either<String, Int>, и конструктор будет ничуть не менее безопасным, чем в Crystal.Конструктор чего? Either<String, Int> или класса в котором он используется?
Crystal выводит тип буквально в каждой строке.
if Random.rand > 0.5
test = "text"
else
test = 5
end
# String | Int
test = test.to_s
# String
puts test.upcase
Rust либо не в состоянии вывести тип вообще, либо только по первой иницаилизации. Как следствие структуру в rust можно инициализировать только в одну строку. Нельзя в одной строке инициализировать одно поле, потом вызвать какой-то метод, потом инициализировать второе поле. Rust не может гарантировать, что вызванный метод не наткнётся на неинициализированное поле. То-есть данный код не может быть перенесён на rust без создания временных переменныхclass Test
@b : Int32
def initialize
@a = 1
@b = plus
@c = 3
end
def plus
@a + 2
end
def print
puts @a + @b + @c
end
end
Test.new.print
Crystal выводит тип буквально в каждой строке.
Rust тоже.
Rust либо не в состоянии вывести тип вообще, либо только по первой иницаилизации.
По первой инициализации.
Конструктор чего? Either<String, Int> или класса в котором он используется?
И того и другого.
В расте нет перегрузки функций, поэтому ваш initialize
с a
типа String | Int
в расте должен выглядеть как fn initialize(a: Either<i32, String>)
.
То-есть данный код не может быть перенесён на rust без создания временных переменных
То есть может. Если вы не разбираетесь в чем-то, спросите у людей, которые знают: https://web.telegram.org/#/im?p=@rust_beginners_ru
Rust тоже.Ага, охотно верим. После первой строки лишь проверяется соответствие уже выведенным. rust способен вывести тип аргументов функции хотя бы в простейшем случае?
В расте нет перегрузки функцийЭто не перегрузка функции.
То есть может. Если вы не разбираетесь в чем-то, спросите у людей, которые знают:А вы что, сами не знаете? Тогда зачем говорите? А если знаете, то почему сами не перепишите мой код на rust?
Это не перегрузка функции.
Error: instance variable '@val' was used before it was initialized in one of the 'initialize' methods, rendering it nilable
И что же это тогда? Документация говорит, что это перегрузка. https://crystal-lang.org/reference/syntax_and_semantics/overloading.html
Подобный код на C++ приведет к еще более любопытным результатам. Вместо вызова функции производного класса будет вызвана функция базового класса. Это имеет немного смысла, потому что производный класс еще не был инициализирован (помните, мы не можем просто сказать, что все поля имеют значение null). Однако если функция в базовом классе будет чистой виртуальной, ее вызов приведет к UB.
Как только на С++ попытаться создать объект наследника, думаю, такой код не отлинкуется вообще, потому что для чисто виртуальной функции базового класса определения не будет. Линкер просто не найдет её… и не соберется ничего, соответственно и вызова не будет.
Из стандарта, 10.4 Абстрактные классы, параграф 6:
Member functions can be called from a constructor (or destructor) of an abstract class; the effect of making a virtual call (10.3) to a pure virtual function directly or indirectly for the object being created (or destroyed) from such a constructor (or destructor) is undefined.
Если написать примитивно (т. е. просто переписать пример c Kotlin на C++), то да, код просто не скомпилируется. Но можно написать так, что все соберется и запустится. Вот пример кода, который приводит к UB.
Подробнее можно почитать на StackOverflow.
Класс у которого ожидаются публичные наследники обязан объявить виртуальный деструктор. Всегда можно создать пустой, но многим хочется сделать его абстрактным. Ну, не знаю, для красоты, что ли. Но если его не определить, то нельзя будет освобождать наследников.
Через умный указатель — можно и без виртуального деструктора.
И да, можно же умный указатель использовать.
Недостаток этого подхода заключается в том, что любой код может создать структуру, так что нет единого места, такого как конструктор, для поддержания инвариантов. На практике это легко решается приватностью: если поля структуры приватные, то эта структура может быть создана только в том же модуле. Внутри одного модуля совсем нетрудно придерживаться соглашения «все способы создания структуры должны использовать метод new».
«джентельменского соглашения»? А вариант с конструкторами точно плохая идея? Тем более что вариант с одним базовым конструктором, выдерживающим инварианты, и несколькими переиспользующими его перегрузками приведен в статье
Опасности конструкторов