Лучший способ сохранить жизнь… временных элементов из выражения C

#c #lifetime #preserve #temporaries

Вопрос:

Рассмотрим двоичную операцию X с перегруженным правоассоциативным оператором C : a =b =c —> Y{a, X{b,c}}

Можно «заморозить» всю информацию об операндах из выражения в некотором синтаксическом дереве (комбинации объектов X amp; Y) и получить доступ к ней позже. (вопрос не в этом)

 struct X{Operandamp; l; Operandamp; r; /*...*/};
struct Y{Operandamp; l; X r; /*...*/};
Operand a, b, c;
auto x = Y{a, X{b,c}};
//access members of x...
 

Если я сохраню Y::r как значение (как указано выше), то будет задействовано копирование или, по крайней мере, перемещение.
Если я сохраню Y::r ссылку rvalue (например Xamp;amp; r; ), то она будет ссылаться на временное значение, которое будет уничтожено, когда выражение закончится, оставив меня с висячей ссылкой.

Каков наилучший способ поймать его или предотвратить это автоматическое уничтожение, чтобы использовать уже построенное выражение несколько раз в нескольких местах?

  • Под перехватом я подразумеваю продление каким-то образом его срока службы, а не ручное присвоение ему локальной переменной (работает, но плохо масштабируется! подумайте об этом деле: a =b =… =z )
  • Я знаю, что переезд дешевле, чем копирование… но ничего не делать еще лучше (объект есть, он уже построен).
  • вы можете передать выражение в качестве ссылочного аргумента rvalue функции или лямбде и получить доступ к его членам, находясь внутри этой функции/лямбды… но вы не можете повторно использовать его (снаружи)! Вы должны воссоздавать его каждый раз (кто-то назвал этот подход «Лямбда судьбы», возможно, есть и другие недостатки)

Вот тестовая программа (в прямом эфире на https://godbolt.org/z/7f78T4zn9):

 #include <assert.h>
#include <cstdio>
#include <utility>

#ifndef __FUNCSIG__
#   define __FUNCSIG__ __PRETTY_FUNCTION__
#endif

template<typename L,typename R> struct X{
    Lamp; l; Ramp; r;
    X(Lamp; l, Ramp; r): l{l}, r{r} {printf("X{this=%p amp;l=%p amp;r=%p} %sn", this, amp;this->l, amp;this->r, __FUNCSIG__);};
    ~X(){printf("X{this=%p} %sn", this, __FUNCSIG__);};

    X(const Xamp; other) noexcept      = delete;
    X(Xamp;amp; other) noexcept           = delete;
    Xamp; operator=(const Xamp;) noexcept = delete;
    Xamp; operator=(Xamp;amp;) noexcept      = delete;
};
template<typename L,typename R> struct Y{
    Lamp; l; Ramp;amp; r;
    Y(Lamp; l, Ramp;amp; r): l{l}, r{std::forward<R>(r)} {
        printf("Y{this=%p amp;l=%p r=%p} %sn", this, amp;this->l, amp;this->r, __FUNCSIG__);
        assert(amp;this->r == amp;r);
    };
    ~Y(){printf("Y{this=%p} %sn", this, __FUNCSIG__);};
    void func(){printf("Y{this=%p} amp;r=%p ... ALREADY DELETED! %sn", this, amp;r, __FUNCSIG__);};
};

struct Operand{
    Operand(){printf("Operand{this=%p} %sn", this, __FUNCSIG__);}
    ~Operand(){printf("Operand{this=%p} %sn", this, __FUNCSIG__);}
};

//================================================================
int main(){
    Operand a, b, c;
    printf("---- 1 expression with temporariesn");
    auto y = Y{a, X{b,c}};//this will come from an overloaded right-associative C   operator, like: a =b =c
    printf("---- 2 immediately after expression... but already too late!n");//at this point the temporary X obj is already deleted
    y.func();//access members...
    printf("---- 3n");
    return 0;
}
 

Вот пример вывода, в котором вы можете увидеть адрес временного объекта X, входящего в Y::r … и уничтожен сразу же после этого, прежде чем у него появился шанс поймать его:

 ---- 1 expression with temporaries
X{this=0x7ffea39e5860 amp;l=0x7ffea39e584e amp;r=0x7ffea39e584f} X::X(Operandamp;, Operandamp;)
Y{this=0x7ffea39e5850 amp;l=0x7ffea39e584d r=0x7ffea39e5860} Y::Y(Operandamp;, Xamp;amp;)
X{this=0x7ffea39e5860} X::~X()
---- 2 immediately after expression... but already too late!
 

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

1. «Если я сохраню Y::r в качестве значения, то будет задействовано копирование или, по крайней мере, перемещение». Это проблема только в вашем тесте, потому что задействованы побочные эффекты. Если это будет использоваться для арифметических задач, велика вероятность того, что все это будет оптимизировано. Единственный способ сказать наверняка — это посмотреть на получившуюся сборку.

2. C не предоставляет средств для манипулирования абстрактным синтаксическим деревом, проверки или отражения.

3. @Frank: речь идет о составлении 2 объектов в целом, а не об арифметике. И кроме копирования и оптимизации возвращаемого значения (которые также гарантированы без активации оптимизации), вы мало что можете сделать… особенно с удаленными копиями и перемещениями и назначением

4. @Solo Я хочу сказать, что это интуитивно похоже на попытку вручную выполнить работу, в которой компиляторы уже очень хороши.

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

Ответ №1:

Нет никакого способа продлить жизнь временных сотрудников так, как вы хотите.

Есть несколько способов продлить временную жизнь. Большинство из них бесполезны. Например, временное значение, используемое при инициализации элемента во время конструктора, сохраняется до конца конструктора. Это может быть полезно в одном «слое» такого дерева выражений, но не помогает для двух.

Один из интересных способов продлить временную жизнь-это стать предметом ссылки.

 {
    const std::stringamp; x = std::string("Hello")   " World";
    foo();
    std::cout << x << std::endl; // Yep!  Still "Hello World!"
}
 

Это будет продолжаться до x тех пор, пока не выйдет за рамки. Но это ничего не даст для продления жизни других временных. "Hello" все равно будет уничтожено в конце этой линии, даже если "Hello world" выживет. И для вашей конкретной цели вам тоже нужно "Hello" .

На данный момент, можете ли вы сказать, что я был разочарован этой проблемой раньше?

Я нашел два последовательных подхода.

  • Управляйте своим деревом путем копирования и перемещения таким образом, чтобы конечное шаблонное выражение действительно содержало объекты (это ответ, которого вы не хотели. Извините)
  • Управляйте своим деревом с помощью умных ссылок, чтобы избежать копий. Затем сделайте невозможным назначение локальной переменной для ее сохранения, удалив конструкторы. Затем используйте операнд только в выражении on (это другой ответ, которого вы не хотели, так как он сам приводит к упомянутым вами лямбда-трюкам).
    • И это полностью нарушается кем-то, кто знает, что вы можете присвоить значение новой локальной ссылке (потому что некорневые временные узлы исчезают). Однако, возможно, это приемлемо. Эти люди знают, кто они такие, и они заслуживают всех неприятностей, которые они получают за то, что пытаются быть особенными (не то, чтобы я был одним из этих людей…)

Я сам делал оба подхода. Я создал движок JSON с временными переменными, который при компиляции с помощью наполовину приличного g или Visual Studio фактически скомпилировался до минимального количества предварительно скомпилированных хранилищ в стеке, необходимых для создания моих структур данных. Это было великолепно (и почти без ошибок…). И я построил скучные структуры» просто скопируйте данные».

Что я нашел? По моему опыту, угол, в котором окупаются такого рода махинации, очень мал, вам нужно:

  • Гипервысокая производительность, когда стоимость этих конструкторов и деструкторов не является тривиальной.
  • В первую очередь есть причина для структуры дерева выражений, например, преобразования DAG, которые не могут быть выполнены с помощью простого лямбда-выражения
  • Код, вызывающий эту библиотеку, должен выглядеть настолько чистым, что вы не можете позволить себе выделять собственные локальные переменные для конечных узлов (таким образом, обходя единственный действительно непоколебимый случай, когда вы не можете просто скопировать что-то в последний момент).
  • Вы не можете полагаться на оптимизатор для оптимизации своего кода.

Обычно один из этих трех случаев дает. В частности, я замечаю, что и STL, и Boost имеют тенденцию использовать подход «копируй все». Функции STL копируются по умолчанию и обеспечивают std::ref , когда вы хотите попасть в неприятную ситуацию в обмен на производительность. В Boost есть довольно много деревьев выражений, подобных этому. Насколько мне известно, все они полагаются на копирование всего. Я знаю Толчок.Феникс делает (Феникс-это в основном завершенная версия вашего оригинального примера), и Boost.Spirit делает то же самое.

Оба этих примера следуют шаблону, которому, я думаю, вы должны следовать: корневой узел «владеет» своими потомками либо во время компиляции с помощью умных шаблонов, в которых операнды являются переменными-членами (а не ссылками на указанные операнды, a. la. Феникс) или во время выполнения (с указателями и распределением кучи).

Кроме того, учтите, что ваш код становится жестко зависимым от компилятора C , идеально соответствующего спецификациям. Я не думаю, что они на самом деле существуют, несмотря на все усилия разработчиков компиляторов, которые лучше меня. Вы живете в крошечном уголке, где «Но это соответствует спецификациям» может быть опровергнуто «Но я не могу скомпилировать его на любом современном компиляторе!»

Я люблю творчество. И, пожалуйста, если вы поймете, как сделать то, что вы хотите, пожалуйста, громко прокомментируйте мой ответ, чтобы я мог учиться на вашем уме. Но из моих собственных усилий по изучению спецификации C в поисках способов сделать именно то, что вы ищете, я почти уверен, что ее там нет.

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

1. Кроме того, подумайте, насколько удручающе сложно было бы создать оптимальную сборку для 99,99% случаев, если бы ей действительно пришлось отслеживать все эти временные параметры в полной среде типа проблемы с остановкой в вашем случае 0,01%. Есть очень веская причина, по которой в C есть выражения и операторы, и они следуют совсем другим правилам.

2. @Evg Спасибо, что исправил эту ошибку. Мне нужно было добавить константу (теперь компилируется в godbolt )

3. Мне неприятно тебе это говорить, но ты прав! 🙂 Похоже, я хочу слишком многого: с текущими функциями языка невозможно обеспечить как возможность повторного использования, так и длительный срок службы для временных пользователей. Улавливание временных значений любым известным мне способом делает выражение не подлежащим повторному использованию! И он не может быть повторно использован с отключенными элементами копирования/перемещения… Спасибо за ваше подробное объяснение и приношу извинения за ваше разочарование… но ты больше не одинок: я только что вступил в клуб! 🙂