Я не могу представить себе настоящий язык RAII, который также имеет оптимизацию хвостового вызова в спецификациях, но я знаю, что многие реализации C ++ могут сделать это как оптимизацию для конкретной реализации.
Это ставит вопрос для тех реализаций, которые делают: учитывая, что деструкторы вызываются в конце области видимости автоматической переменной и не с помощью отдельной процедуры сборки мусора, не нарушает ли ограничение TCO, что рекурсивный вызов должен быть последней инструкцией в конце функции?
Например:-
#include <iostream>
class test_object {
public:
test_object() { std::cout << "Constructing...\n"; }
~test_object() { std::cout << "Destructing...\n"; }
};
void test_function(int count);
int main()
{
test_function(999);
}
void test_function(int count)
{
if (!count) return;
test_object obj;
test_function(count - 1);
}
«Построение …» будет написано 999 раз, а затем «Разрушение …» еще 999 раз. В конечном счете, 999 test_object
экземпляры будут автоматически распределяться перед размоткой. Но если предположить, что реализация имеет TCO, будет 1000 кадров стека или только 1?
Деструктор после рекурсивного вызова вступает в противоречие с требованиями реализации defacto TCO?
Если принять во внимание номинальную стоимость, то, похоже, RAII работает против TCO. Однако помните, что существует ряд способов, которыми компилятор может, так сказать, «сойти с рук».
Первый и наиболее очевидный случай — если деструктор тривиален, то есть он является деструктором по умолчанию (генерируемым компилятором), и все дочерние объекты также имеют тривиальные деструкторы, тогда деструктор фактически не существует (всегда оптимизируется). В этом случае TCO может быть выполнен как обычно.
Затем деструктор может быть встроен (его код берется и помещается непосредственно в функцию, а не вызывается как функция). В этом случае все сводится к тому, чтобы иметь некоторый код «очистки» после оператора return. Компилятору разрешается переупорядочивать операции, если он может определить, что конечный результат является тем же (правило «как если»), и он будет делать это (в общем), если переупорядочение приводит к улучшению кода, и я бы предположил, что TCO является одним из соображений, применяемых большинством компиляторов (т. е. если он может изменить порядок вещей таким образом, что код станет подходящим для TCO, то он это сделает).
А в остальных случаях, когда компилятор не может быть «достаточно умен», чтобы делать это самостоятельно, ответственность за это ложится на программиста. Наличие этого автоматического вызова деструктора усложняет программисту видеть код очистки, запрещающий TCO, после хвостового вызова, но это не имеет никакого значения с точки зрения способности программиста сделать назначить кандидата на TCO. Например:
void nonRAII_recursion(int a) {
int* arr = new int[a];
// do some stuff with array "arr"delete[] arr;
nonRAII_recursion(--a); // tail-call
};
Теперь наивный RAII_recursion
реализация может быть:
void RAII_recursion(int a) {
std::vector<int> arr(a);
// do some stuff with vector "arr"RAII_recursion(--a); // tail-call
}; // arr gets destroyed here, not good for TCO.
Но мудрый программист все еще может увидеть, что это не сработает (если только не встроен векторный деструктор, что вполне вероятно в этом случае), и может легко исправить ситуацию:
void RAII_recursion(int a) {
{
std::vector<int> arr(a);
// do some stuff with vector "arr"}; // arr gets destroyed here
RAII_recursion(--a); // tail-call
};
И я вполне уверен, что вы могли бы продемонстрировать, что практически нет случаев, когда этот вид трюка не мог бы быть использован для обеспечения возможности применения TCO. Таким образом, RAII лишь усложняет просмотр возможности применения TCO. Но я думаю, что программисты, которые достаточно мудры, чтобы разрабатывать рекурсивные вызовы с поддержкой TCO, также достаточно мудры, чтобы видеть те «скрытые» вызовы деструкторов, которые должны были бы произойти перед хвостовым вызовом.
ДОБАВЛЕННОЕ ПРИМЕЧАНИЕ: посмотрите на это так, деструктор скрывает некоторый код автоматической очистки. Если вам нужен код очистки (т. Е. Нетривиальный деструктор), он понадобится вам независимо от того, используете ли вы RAII или нет (например, массив в стиле C или любой другой). И затем, если вы хотите, чтобы TCO был возможен, должна быть возможность выполнить очистку перед выполнением хвостового вызова (с RAII или без него), и это возможно, тогда возможно принудительное уничтожение объектов RAII. перед хвостовым вызовом (например, поместив их в дополнительную область).
Если компилятор выполняет TCO, порядок вызова деструкторов изменяется в зависимости от того, когда он не выполняет TCO.
Если компилятор может доказать, что это переупорядочение не имеет значения (например, если деструктор тривиален), то согласно как будто Правило может выполнять TCO. Однако в вашем примере компилятор не может доказать это и не будет выполнять TCO.