Как стать автором
Обновить

Комментарии 103

НЛО прилетело и опубликовало эту надпись здесь

UPD:
В комменте был ответ про типаж Default, но в изначальном комменте этот вопрос был удален

НЛО прилетело и опубликовало эту надпись здесь
Пример, приведенный для С++, укуренный неверный бред
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. зачем и почему
Почему вы думает что никто кроме вас не знает как правильно использовать std::move? Просто приведите пример кода: как правильно передавать аргументы в конструктор класса из примера, по вашему мнению.

Ну синтаксис у автора действительно немного странный, хоть и рабочий. Обычно в подобных случаях используют константные ссылки в качестве аргументов, что позволяет обойтись без странных костылей с 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 с полями класса. Исходные строки остались нетронуты.
Думаю, автор и math_coder имели в виду что-то вроде godbolt.org/z/vWy5eS. Вы правы в том, что копия иногда будет создаваться (зависит от caller — если он будет передавать rvalue reference, то копирование будет заменено на еще одно перемещение), но автор имел в виде не отсутствие копирования, а «семантику», когда само «значение» явно «захватывается» (перемещается когда это возможно сделать безопасно и копируется в остальных случаях) callee и дальше уже callee сам контролирует life time этого значения.

«Передача владения» здесь и вправду звучит несколько неверно, потому что в C++ (и не только) под этим словами обычно понимают иное.
А зачем вы так прямо произвольно заменили std::string m1, m2 на std::string &m1, &m2 (у автора такого не было)? Вы же понимаете разницу, я надеюсь? Если этой замены не производить, то все точно так же сработает прекрасно, при создании m1 и m2 вызовутся конструкторы копий и будет опять же произведено безопасное копирование, и std::move() не нужен.

P.S. А, вы имеете в виду, что этот синтаксис дает выбор между foo(arg1, arg2) и foo(std::move(arg1), std::move(arg2)). Тогда согласен.
да, основная фишка именно в возможности выбора между foo(arg1, arg2) и foo(std::move(arg1), std::move(arg2)), в стандартном варианте без мувов нужно будет 2 конструктора для того же функционала (один с const string& для копирования, второй с неконстантной ссылкой и свапом внутри для передачи владения)

Блин до чего же в плюсах запутаная move семантика.

Не два, а 4: каждый аргумент может как перемещаться, так и копироваться независимо.

Кстати, пример у автора забавный еще и в том смысле, что члены класса типа std::string как class type variables таки имеют вполне себе «разумные значения по умолчанию», так как для них будут вызваны конструкторы по умолчанию, даже если они и не упомянуты в списке инициализации. Вот если бы он какие-нибудь int'ы вместо них засандалил, вот тогда это действительно было бы примером «отсутствия разумного значения по умолчанию» в данном случае :)

Передача владения и копирование — это вещи ортогональные. Вы сделали копии и передали f во владение копии. Если вам по логике работы программы необходимо иметь две копии соответствующих строк, естественно, что одного копирования не избежать. Если же вам не нужны две копии — надо использовать std::move:


foo f(std::move(arg1), std::move(arg2))`
Ну все правильно, о том и разговор. Если это сеттер (т.е. аргументы полностью копируются для дальнейшего использования как есть), то такой синтаксис оптимален со всех точек зрения: person(std::string first_name, std::string last_name). Если это не сеттер (т.е. аргумент не копируется или используется частично), то такой синтаксис оптимален: person(const std::string& first_name, const std::string& last_name). Из примера ясно видно что это именно первый вариант. Сложно? Ну да, нужно всегда думать что, зачем и куда вы передаете, впрочем как и в Rust.

Оптимален со всех точек зрения — это вряд ли, специализированные конструкторы (которые будут принимать 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 в данном случае вам никак не поможет.
Ну да, в таком случае оптимизатору компилятора негде развернуться. Придется вызывать std::cout. Кстати забавный у вас конструктор перемещения arg_t(const arg_t&&). А что так тоже можно?
Ну да, в таком случае оптимизатору компилятора негде развернуться.


Ну это мягко говоря не так, в случаях, когда 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 — будет ровно то же самое :)
Ок. Согласен. Но даже если не рассматривать copy elision (который действует строго по стандарту, как и RVO/NRVO — есть паттерн — тупо выкидываем...), вы действительно считаете что современный компилятор будет переливать из пустого в порожнее, т.е. сначала скопирует строчку во временную область на стеке, а потом ее же скопирует во внутренний буфер? Когда меня действительно волновала производительность, в боевом коде, я смотрел выхлоп компилятора и я не помню что бы такое происходило. Случаи конечно бывают разные, но зачем заморачиваться такими вещами, в общем случае?
Ну мне как раз вот этот типа «универсальный» вариант как раз и не нравится вот этим потенциальным переливанием из пустого в порожнее. Лучше написать специализированные конструкторы. Хотя, конечно, там, где перемещение дешевое, можно и так, и надеяться, что компилятор реализует все это как-то пооптимальнее.
Ну хорошо, а какие у нас есть варианты?! Если уж сильно «печёт» и профайлер указал именно на перемещение, то можно конечно сделать ваш — назовем условно «ручной вариант», но представьте себе такую сигнатуру: person(std::string first_name, std::string last_name,, std::string nick_name);
Что, будем реально делать восемь перегрузок, на все комбинации?
Ну простор для творчества тут есть, можно, например, не валить все в конструктор, а сделать оптимизированные сеттеры для «жирных» полей, один будет принимать const T&, второй T&&. Ну понятно, что те, кто использует конструкторы такого типа, в общем случае разменивают удобство написания кода на скорость работы.

Специально для этого придумали std::forward

И как же вы его будете использовать?

Я ниже привел пример, как.
Интересно, что это за минусяторы, которые минусуют за рабочий пример perfect forwarding constructor'а :)

Вы как-то очень странно отвечаете: спорите одновременно и со мной, и с моим оппонентом — и все одним и тем же примером.

Да я вроде не спорю, вы спросили, как тут можно использовать std::forward, я привел ссылку на рабочий пример. И с вашим оппонентом я не спорю, я согласен, что если бы не озвученное ограничение «шаблоны не нужны», то std::forward вполне годится. Я вообще не в курсе, что вы оппоненты :)

Это был риторический вопрос. На риторические вопросы отвечать не обязательно.

Выше Videoman сказал, что требуется сделать без шаблонов (ну, я так понял, по крайней мере), а с std::forward и универсальными ссылками без шаблонов не обойтись. А так да, конечно, без проблем:

godbolt.org/z/Ceqiwi
НЛО прилетело и опубликовало эту надпись здесь
Люблю C++.
Каждый раз, когда читаю такие обсуждения, радуюсь, что в своё время отказался изучать плюсы и ограничился чистым С. Там, если прострелил ногу, хотя бы видно дуло, как минимум его срез.
Ой, ну началось :) Нормальный язык. Ну да, посложнее, чем «чистый C», так ведь и умеет побольше.
НЛО прилетело и опубликовало эту надпись здесь
Оптимален со всех точек зрения — это вряд ли, специализированные конструкторы (которые будут принимать lvalue или rvalue ссылки) не будут допускать лишних вызовов конструкторов для своих аргументов, а тут частенько будут 2 вызова, когда в реальности достаточно одного. Ну, зато меньше кода писать, это да. Как швейцарский нож — годится для всего, но одинаково плохо.

лишний move — не такая уж большая беда, тем более при фиксированном типе аргумента, у которого быстрый мув (а-ля std::string).

Вы забываете одну офигенно важную вещь, порождаему именно таким способом передачи аргументов — полученный конструктор безопасен относительно исключений. Если передавать константные ссылки — мы не получим такой фичи)

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

Ну вот, кстати, хорошее замечание от MooNDeaR. Я совсем об этом забыл, т.к. по умолчание всегда так передаю аргументы. А как вы сделает конструктор принимающий константные ссылки безопасным? Ведь придется копировать и, скорее всего, выделять память. И тут мы приходим опять к дилемме: либо ограничения связанные с невозможностью сообщить о проблеме, либо двойная инициализация. А еще особая веселуха начинается когда мы начинаем менять класс, добавлять/удалять туда сюда конструкторы/операторы копирования/перемещения. В случае передачи по значению все остается как и было (noexcept не страдает). В случае с ручным подходом, у нас начинает «плясать» весь интерфейс от этого зависящий.
Так вы про какую безопасность, про noexcept что ли? Так я же вам выше продемонстрировал, что при таком способе передачи аргументов, если вызывающий не использует std::move(), то точно так же вызываются конструкторы копий, которые могут «скорее всего выделять память» и так далее.
Все верно, вызывается. Но вызывается он до схода в конструктор, при копировании самого аргумента. потом делается только move, который легко сделать безопасным. Т.е сам конструктор noexcept. Если возникнет исключение, то до входа в конструктор.
Проиллюстрирую — вот такой код безопасен относительно исключений:
SomeClass& SomeClass::operator=(SomeClass that) noexcept
{
    swap(that);

    return *this;
}
Так вы хотите чтобы исключений не было, или чтобы просто конструктор был noexcept? :) Ну да, noexcept конструктор при таком подходе сделать будет нельзя, но исключения все равно вполне себе будут в любом случае.
Под безопасностью, по умолчанию, подразумевается базовая или строгая, т.е. насколько сохраняется инвариант класса в случае, если происходит исключение:
небезопасный — класс может быть в любом состоянии в случае исключения, в том числе потерять свой инвариант. UB короче.
базовая безопасность — класс может поменять состояние, но инвариант сохраняется
строгая безопасность — класс откатывается в точности в то состояние, в котором он был до вызова метода который вызвал исключение.
В случае передачи по значению, вы автоматом обеспечиваете строгую безопасность, грубо говоря, откатываете транзакцию, и вам не нужно ни о чем думать.
Эээ, минуточку. Если у вас исключение возникает в конструкторе, то ни о каких «сохранениях инварианта» и речи быть не может, поскольку класс как таковой не создается вообще. Будут автоматически вызваны деструкторы для тех членов класса, для которых уже успели вызваться конструкторы, но деструктор того класса, в конструкторе которого произошло исключение, не будет вызван потому, что он еще не был создан. Там, возможно, нужно быть аккуратным с внешними ресурсами типа файловых дескрипторов, если вы их используете «прям так» без RAII, в общей куче полей класса, так сказать, но при аккуратном написании проблем там минимум.
Здесь конструктор — это только частный случай. Я говорил об сеттерах вообще, передаче параметров по значению и о конструкторах в частности. Так что инвариант класса, вам этот подход, очень даже помогает поддерживать. Я даже привел пример кода. Просто мы говорим о передаче параметров по значению. То что вы описали это прописные истины и без их знания код безопасный к исключениям вообще не напишешь.
но при аккуратном написании проблем там минимум.
Вот для аккуратного написания, желательно, правильно обходиться с исключениями, следить за ресурсами, минимизировать «ручное» управление и использовать 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: от того момента, пока были изобретены аьстракции, которые, вроде как, чисто теоретически, ничего не должны были стоить до того момента, пока они реально перестали чего-либо стоить — прошло много лет. Тут — примерно та же история.

Иногда, действительно, для борьбы с существующими компиляторами приходится использовать ссылки. Ну тут нужно понимать — почему и зачем.
Как показала последовавшая дискуссия, действительно, знает только KanuTaH =)

Полный правильный ответ находится в учебнике Мейерса «Эффективный и современный С++» Глава 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)?
Интересно, минусующие не поняли что это ошибки компиляции или им не интересен язык в котором решена проблема с null pointer exception и присутствуют конструкторы?

Именно в такой постановке вопроса — нет, не интересен. Наличие конструкторов — не самоцель.

А чем это так принципиально отличается от подхода Rust, кроме того, что null в целом из основной части языка никуда не делся, и фактически все nullable-значения, судя по этому куску кода, ведут себя как Option?

Crystal значительно превосходит Rust в плане вывода типов. В Crystal, как и в Haskell(возможно и в других языках) возможно написать программу ни разу не указав конкретный тип и программа будет защищена статической типизацией. Rust довольно плохо выводит типы и может сломаться на простейшем коде.
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
Не очень понял, какой тип, если это статическая типизация, выведет Кристалл для test
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 несколько… эээ… шизофренистичен. С одной стороны он не поддерживает автоматического создания «сложных» типов, которые не ложатся однозначно «на железо»… с другой стороны — он не даёт жёсткого описания того, как типы, которые в нём таки есть, на него ложатся.

Было бы разумно сделать выбор либо в одну сторону, либо в другую… но пока — вот так.
с другой стороны — он не даёт жёсткого описания того, как типы, которые в нём таки есть, на него ложатся

А зачем на пустом месте закрывать будущие возможности оптимизации?

А зачем на пустом месте закрывать будущие возможности оптимизации?
А почему тогда не разрешить вещи типа описанного String | Int?

Такие типы с обобщенными типами дружат плохо. К примеру, какой-нибудь T | Int при T = Int совсем не равен Int | Int.

А чему он равен?
Именно по этой причине систем типов 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

Перегрузка, это когда есть несколько объявлений с разными аргументами. В том примере все функции объявлены однократно.

ЗЫ где аналогичный код на rust, если он в состоянии выразить это?
Подобный код на 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.

Спасибо, понял. Я да, имел ввиду вызов чисто виртуальной метода прямо в конструкторе. Об вызове через обычную не подумал даже.
А уж тем более, чтобы вызвать чисто виртуальный метод, который можно определить.
НЛО прилетело и опубликовало эту надпись здесь
Так он не сможет указатель на неё определить для таблицы. Ему же надо туда что то записать… Но оказывается, мне выше ссылку дали, StackOverflow можно чисто виртуальную функцию определить. И тогда указатель будет и UB будет.
Неужели Вам ни разу не приходилось определять абстрактный виртуальный деструктор?!
В реальном коде ни разу. Как, впрочем, и обычный деструктор. Хотя нет, обычный определял, RAII для критической секции и мьтексов.
Видимо, Вам не приходилось создавать иерархии классов. Вообще-то эта gotcha описывается в начале книг типа Effective C++.
Класс у которого ожидаются публичные наследники обязан объявить виртуальный деструктор. Всегда можно создать пустой, но многим хочется сделать его абстрактным. Ну, не знаю, для красоты, что ли. Но если его не определить, то нельзя будет освобождать наследников.

Через умный указатель — можно и без виртуального деструктора.

В смысле через умный указатель на базовый класс можно корректно удалить объект производного класса?

Именно так.

Задавая при создании в делитере статический вызов деструктора фактического типа?
НЛО прилетело и опубликовало эту надпись здесь
Это я знаю, просто зачем его объявлять, если объект никогда не уничтожается… Статика, сплошная статика. Деструктор только если на стеке объект, да и то, так для баловства для критической секции, чтобы кода меньше писать.
И да, можно же умный указатель использовать.
У каждого свой C++)))
Недостаток этого подхода заключается в том, что любой код может создать структуру, так что нет единого места, такого как конструктор, для поддержания инвариантов. На практике это легко решается приватностью: если поля структуры приватные, то эта структура может быть создана только в том же модуле. Внутри одного модуля совсем нетрудно придерживаться соглашения «все способы создания структуры должны использовать метод new».

«джентельменского соглашения»? А вариант с конструкторами точно плохая идея? Тем более что вариант с одним базовым конструктором, выдерживающим инварианты, и несколькими переиспользующими его перегрузками приведен в статье
НЛО прилетело и опубликовало эту надпись здесь
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории