2

我写了一个作用域守卫,它在作用域退出时重置一个值:

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. 无法将格式化选项(或任何参数,实际上)传递给隐式转换函数。因此,我不得不求助于使用全局变量。

4

3 回答 3

1

对不起,我之前的回答,伙计们,我想什么?我应该正确阅读这个问题。

因此,当然,foo()必须返回您的ResetGuard对象以延长其生命周期,这是一件好事,而不是一件坏事。

首先,它对调用者几乎没有负担。毕竟,他/她要做的就是:

auto rg = foo ();

作为我的潜在呼叫者,foo()我绝对不会有任何问题,@melpomene 在上面的评论中的出色建议 ( [[nodiscard]]) 可用于确保呼叫者不会忘记这样做。

为什么强迫呼叫者这样做是一件好事(除了你在这件事上别无选择)?好吧,它让调用者有机会管理 scopeguard 的生命周期,这可能很有用(很快就会提供现场演示)。

至于这里的其他答案,我绝对不会将所有这些隐藏在宏中,因为这会隐藏潜在调用者的重要信息foo()。相反,我会用它[[nodiscard]]来提醒他们他们的责任,然后把它留在那里。

[编辑]

我现在在 Wandbox 上花了一点时间来完善代码,以添加全套推荐的构造函数/赋值运算符并演示 的用法[[nodiscard]],这对我来说是当天的发现。

首先,修改类,以(我相信)知道的人推荐的方式完成。我可以特别看到定义正确的移动构造函数的重要性(想想如果不这样做可能会遇到的细微错误)。从 JVApen 捏了一些东西 ( = delete),对我来说看起来很明智,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 和保证复制省略,这一切都很好而且高效,所以也不需要担心。

现场演示

于 2018-07-01T08:31:19.927 回答
1

有什么方法可以延长 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++ 程序员来说,只要提供一个正确命名的函数就可以了。

于 2018-07-01T07:13:36.927 回答
1

在回答你的问题之前,我想提供用 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 引用捕获临时文件来扩展临时文件。

const auto &guard = temporarily_set_global_value(15);

但是,我不清楚您总共将拥有多少个实例。但是,如果您使用带有移动构造函数的解决方案,这不再是问题。此外,当您使用编译器优化时,该函数可以在头文件中实现时内联。这可以消除所有副本。

于 2018-07-01T12:34:20.767 回答