Хорошо ли определено поведение объекта подкласса по значению для функции, принимающей объект суперкласса?

Я рассмотрел связанные с этим вопросы, такие как здесь и здесь об этой теме, и все они описать разбиение объектов, но ни один из них не определяет, является ли он безопасным, надежным и предсказуемым.

Есть ли гарантия от стандартного или компилятора, если я передаю объект подкласса по значению методу, который хочет суперкласс, части, которые разрезаны, являются точно членами подкласс, и я смогу использовать нарезанный объект суперкласса без каких-либо проблем в отношении поведения undefined?

Ответ 1

Да, это безопасно, надежно и предсказуемо, потому что оно хорошо определено стандартом (он просто скопирует конструкцию объекта базового класса из объекта производного класса).

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

Ответ 2

Когда они говорят, что "части отрезаны", они не означают, что эти части как-то "исчезают": все они означают, что эти части не копируются в объект, представленный вашей функции для соответствующего параметра. В этом смысле нарезка объектов не является опасной или плохо определенной.

Что происходит, достаточно просто: для того, чтобы построить значение параметра, которое вы передаете по значению, объект производного класса, используемый для фактического параметра, присваивается конструктору базового класса для создания копии, Как только конструктор копирования завершит свою работу, у вас есть объект базового класса.

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

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

void foo(bar b) {
    ... // Payload logic
}

предоставляет вам ту же функциональность, что и

void foo(const bar &r) {
    bar b(r);
    ... // Payload logic
}

С первым фрагментом очень легко упустить тот факт, что амперсанд отсутствует после имени типа, заставляя читателей думать, что полиморфное поведение сохраняется, а оно фактически утрачено. Второй фрагмент легче понять людям, которые поддерживают ваш код, потому что он делает копию явно.