Есть ли способ продлить время жизни временного объекта в C ++? - PullRequest
0 голосов
/ 01 июля 2018

Я написал защиту области, которая сбрасывает значение при выходе из области:

template <class T>
struct ResetGuard
{
    T old_value;
    T& obj_to_reset;
    ResetGuard(T& obj_to_reset, const T& new_value) :
        old_value(obj_to_reset),
        obj_to_reset(obj_to_reset)
    {
        obj_to_reset = new_value;
    }

    ~ResetGuard() { obj_to_reset = old_value; }
};

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

Например:

int GLOBAL_VALUE = 0;
ResetGuard<int> temporarily_set_global_value(int new_val) {
    return { GLOBAL_VALUE, new_val }; //updates the global variable
}
void foo() {
    //Ideally, someone calling this function
    //Wouldn't have to save the returned value to a local variable
    temporarily_set_global_value(15);
    std::cout << "GLOBAL_VALUE is " << GLOBAL_VALUE << std::endl;
}

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

Некоторый контекст вокруг того, что я пытаюсь сделать

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

Я принял решение осторожно использовать глобальную переменную. Альтернативой использованию глобальной переменной будет передача объекта, содержащего спецификацию форматирования. Эта опция в конечном итоге оказалась недостижимой: моя библиотека предназначена для работы с любыми объектами, которые обеспечивают неявное преобразование в std::string. Нет способа передать параметры форматирования (или любые другие параметры) функции неявного преобразования. Следовательно, мне пришлось прибегнуть к использованию глобальной переменной.

Ответы [ 3 ]

0 голосов
/ 01 июля 2018

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

Итак, конечно, foo() должен вернуть ваш ResetGuard объект, чтобы продлить срок его службы, , и это хорошая вещь , а не плохая вещь.

Во-первых, это вряд ли обременительно для звонящего. В конце концов, все, что он / она должен сделать, это:

auto rg = foo ();

В качестве потенциального абонента foo() у меня не было бы абсолютно никаких проблем с этим, и предложение @ mel5men превосходное в комментариях выше ([[nodiscard]]) может быть использовано для того, чтобы вызывающие абоненты не забудьте сделать это.

И почему принуждение вызывающего абонента делать это хорошо (не считая того, что у вас все равно нет выбора)? Что ж, это дает абоненту возможность управлять временем жизни охраны области видимости, и это может быть полезно (скоро появится живая демоверсия).

Что касается других ответов здесь, я бы определенно не скрывал все это в макросе, потому что он скрывает важную часть информации от потенциальных абонентов foo(). Вместо этого я бы использовал [[nodiscard]], чтобы напомнить им об их обязанностях и оставить все как есть.

[Изменить]

Я потратил немного времени на Wandbox, дорабатывая код, чтобы добавить полный набор рекомендуемых конструкторов / операторов присваивания и продемонстрировать использование [[nodiscard]], что для меня является находкой дня.

Во-первых, модифицированный класс, сделанный таким образом (я полагаю), который рекомендуют те, кто знает. Я особенно вижу важность определения правильного конструктора перемещения (просто подумайте о тонких ошибках, с которыми вы можете столкнуться, если не сделаете этого). Ущипнул кое-что (= delete) от JVApen, мне кажется мудрым, TU JV.

#include <iostream>
#include <assert.h>

#define INCLUDE_COPY_MOVE_SWAP_STUFF

template <class T> class [[nodiscard]] ResetGuard
{
public:
    ResetGuard (T& obj_to_reset, const T& new_value) : old_value (obj_to_reset), obj_to_reset (obj_to_reset)
    {
        obj_to_reset = new_value;
    }

#ifdef INCLUDE_COPY_MOVE_SWAP_STUFF
   ResetGuard (const ResetGuard& copy_from) = delete;
   ResetGuard &operator= (const ResetGuard& copy_assign_from) = delete;
   ResetGuard &operator= (ResetGuard&& move_assign_from) = delete;  

    ResetGuard (ResetGuard&& move_from) : old_value (move_from.old_value), obj_to_reset (move_from.obj_to_reset)
    {
        assert (!move_from.defunct);
        move_from.defunct = true;
    }
#endif

    ~ResetGuard()
    {
        if (!defunct)
            obj_to_reset = old_value;
    }

private:
    T old_value;
    T& obj_to_reset;
    bool defunct = false;
};

Прокомментируйте #define INCLUDE_COPY_MOVE_SWAP_STUFF, чтобы увидеть предупреждение компилятора, которое вы получите, если не будете делать все то, что должны.

Тестовая программа:

int GLOBAL_VALUE = 0;

ResetGuard<int> temporarily_set_global_value (int new_val)
{
    return { GLOBAL_VALUE, new_val }; // updates GLOBAL_VALUE
}

void bad_foo()
{
    temporarily_set_global_value (15);
    std::cout << "GLOBAL_VALUE in bad_foo () is " << GLOBAL_VALUE << std::endl;
}

void good_foo()
{
    auto rg = temporarily_set_global_value (15);
    std::cout << "GLOBAL_VALUE in good_foo () is " << GLOBAL_VALUE << std::endl;
}

auto better_foo()
{
    auto rg = temporarily_set_global_value (15);
    std::cout << "GLOBAL_VALUE in better_foo () is " << GLOBAL_VALUE << std::endl;
    return rg;
}

int main ()
{
    bad_foo ();
    good_foo ();
    std::cout << "GLOBAL_VALUE after good_foo () returns is " << GLOBAL_VALUE << std::endl;

    {
        auto rg = better_foo ();
        std::cout << "GLOBAL_VALUE after better_foo () returns is " << GLOBAL_VALUE << std::endl;

        {
            auto rg_moved = std::move (rg);
            std::cout << "GLOBAL_VALUE after ResetGuard moved is " << GLOBAL_VALUE << std::endl;
        }            

        std::cout << "GLOBAL_VALUE after ResetGuard moved to goes out of scope is " << GLOBAL_VALUE << std::endl;
        GLOBAL_VALUE = 42;
    }

    std::cout << "GLOBAL_VALUE after ResetGuard moved from goes out of scope is " << GLOBAL_VALUE << std::endl;
}

Выход компилятора:

prog.cc: In function 'void bad_foo()':
prog.cc:47:38: warning: ignoring returned value of type 'ResetGuard<int>', declared with attribute nodiscard [-Wunused-result]
     temporarily_set_global_value (15);
                                      ^
prog.cc:40:17: note: in call to 'ResetGuard<int> temporarily_set_global_value(int)', declared here
 ResetGuard<int> temporarily_set_global_value (int new_val)
                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
prog.cc:6:40: note: 'ResetGuard<int>' declared here
 template <class T> class [[nodiscard]] ResetGuard
                                        ^~~~~~~~~~

Вывод программы:

GLOBAL_VALUE in bad_foo () is 0
GLOBAL_VALUE in good_foo () is 15
GLOBAL_VALUE after good_foo () returns is 0
GLOBAL_VALUE in better_foo () is 15
GLOBAL_VALUE after better_foo () returns is 15
GLOBAL_VALUE after ResetGuard moved is 15
GLOBAL_VALUE after ResetGuard moved to goes out of scope is 0
GLOBAL_VALUE after ResetGuard moved from goes out of scope is 42 

Так что у вас это есть. Если вы делаете все то, что должны делать (и я надеюсь, что у меня есть!), То все работает просто отлично, и все это хорошо и эффективно благодаря RVO и гарантированному разрешению копирования, так что об этом также не нужно беспокоиться.

Живая демоверсия .

0 голосов
/ 01 июля 2018

Прежде чем ответить на ваш вопрос, я хотел бы предоставить правильный способ решения этой проблемы в C ++.

template <class T>
struct [[nodiscard]] ResetGuard
{
    T old_value;
    T& obj_to_reset;
    bool enabled{true};

    ResetGuard(T& obj_to_reset, const T& new_value) :
       old_value(obj_to_reset),
       obj_to_reset(obj_to_reset)
    {
       obj_to_reset = new_value;
    }

    ResetGuard(ResetGuard &&rhs)
       : old_value(rhs.old_value)
       , obj_to_reset(obj_to_reset)
    {
        rhs.enabled = false;
    }
    ~ResetGuard()
    {
        if (enabled)
            obj_to_reset = old_value;
    }
    ResetGuard(const ResetGuard &) = delete;
    ResetGuard &operator=(const ResetGuard &) = delete;
    ResetGuard &operator=(ResetGuard &&) = delete;  
};

void foo() {
    auto guard = temporarily_set_global_value(15);
    std::cout << "GLOBAL_VALUE is " << GLOBAL_VALUE << std::endl;
}

Приведенный выше код содержит несколько интересных элементов:

  • [[nodiscard]] предотвращение создания временных объектов без создания переменной для обеспечения области действия
  • Член активирован: предотвращение Dtor временного, чтобы иметь побочный эффект
  • Конструктор перемещения: Конструктор перемещения позволяет переместить ResetGuard в другую область с правильной обработкой. В этом случае отключаем старый ResetGuard

В качестве дополнительной заметки я хотел бы обратить внимание на расширение C ++ 17 (ранее разрешенная оптимизация), которое называется Guaranteed Copy / Move Elision . Это гарантирует, что на практике не будет никаких дополнительных временных экземпляров.

Возвращаясь к вашему вопросу: Есть ли способ продлить время жизни временного объекта в C ++?

Да, благодаря N0345 (Предложение от 1993 года). Это предложение позволяет продлить временное, захватив его с помощью константной ссылки.

const auto &guard = temporarily_set_global_value(15);

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

0 голосов
/ 01 июля 2018

Есть ли способ продлить время жизни временного объекта в C ++?

Только один способ, присвоить его переменной (возможно, ссылку). Если вы не хотите обременять пользователей библиотеки, вы можете скрыть детали за макросом. Хотя это правда, что использование макросов становится редким явлением, это то, что вы можете делать только с макросом . Например, вот как вы могли бы сделать это с добавлением расширений GCC:

#define CONCAT(a, b) a##b
#define SCOPED_GLOBAL_VALUE(x) \
  auto&& CONCAT(_unused, __COUNTER__) __attribute__((unused)) = temporarily_set_global_value(x)

Так что теперь, когда пользователи пишут:

SCOPED_GLOBAL_VALUE(15);

Они получают переменную бесплатно с необходимой вам выразительностью.

Но, конечно, есть оговорка. Поскольку мы генерируем имя переменной с помощью препроцессора, мы не можем использовать этот макрос во встроенной функции. Если мы это сделаем, мы нарушим одно определение правила. Так что это нужно учитывать.

Лично я бы не стал это подчеркивать. Это распространенная идиома - требовать именованный объект RAII (подумайте lock_guard), поэтому просто представить правильно названную функцию будет просто для любого опытного программиста C ++.

...