有没有办法延长C++中临时对象的生命周期?

3
我写了一个作用域保护器,当作用域退出时重置一个值:
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
能否正确实现移动构造函数呢?(提示:不要忘记五个法则) - JVApen
2
(这听起来太可怕了)你为什么要避免使用局部变量?这是处理这种情况的常用习语。 - Mat
1
这是一个最小工作示例。我试图避免使用本地变量,因为我希望尽可能使代码健壮:我不希望由于某人忘记将作用域保护保存到本地变量而导致失败。 - Antonio Perez
1
为什么不将其标记为struct [[nodiscard]] ResetGuard - melpomene
1
听起来像是一个xy问题... - Aconcagua
显示剩余4条评论
3个回答

2
在回答你的问题之前,我想先提供一种在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(rhs.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]]:防止创建临时变量而不创建变量以确保作用域 - 成员 enabled:防止临时对象的析构函数产生副作用 - 移动构造函数:移动构造函数允许将 ResetGuard 移动到不同的作用域,并进行正确处理。在这种情况下,禁用旧的 ResetGuard
额外提一下,我想要注意一个 C++17 的扩展(之前允许的优化),称为Guaranteed Copy/Move Elision。这将确保实际上不存在额外的临时实例。
回到你的问题:“有没有办法延长 C++ 中临时对象的生命周期?”
是的,多亏了N0345(1993 年的提案)。该提案允许通过捕获 const 引用来延长临时对象的生命周期。
const auto &guard = temporarily_set_global_value(15);

然而,对于我来说,你总共会有多少个实例还不清楚。不过,如果你使用带有移动构造函数的解决方案,这就不再是问题了。此外,当你使用编译器优化时,如果在头文件中实现该函数,它可以被内联。这样可以消除所有的拷贝。

我认为即使你简单地说auto guard = temporarily_set_global_value(15),也不会有任何副本。当你这样做时,对象实际上是在调用站点分配的,并且temporarily_set_global_value将通过传递给它的隐藏指针在原地构造它,请参见:这里(有一些冗长的帖子,很抱歉)。现在基本上不是问题。 - Paul Sanders
@Paul:我熟悉amd64,那确实是这种情况。不确定其他配置如何。 - JVApen
@JV 我认为现在实际上是必需的,请参见https://en.cppreference.com/w/cpp/language/copy_elision。 - Paul Sanders
只有在使用c++17编译时才会出现这种情况。 - JVApen
是的,你说得对。但即使你回溯一段时间,它似乎仍然以这种方式实际工作,参见https://wandbox.org/permlink/rUbqZ1lWawb2aWXC,所以我认为这只是编译器已经做了相当长时间的形式化表达。 - Paul Sanders
显示剩余2条评论

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++程序员呈现一个正确命名的函数将是直接的。

嗯?为什么不使用内联函数?作用域规则与非内联函数相同,对吧? - Aconcagua
2
@Aconcagua - 如果您在两个不同的翻译单元中包含包含内联函数的头文件,则变量在每个翻译单元处理后可能会有不同的名称。这将违反“完全相同的标记序列”要求。 - StoryTeller - Unslander Monica
1
啊,我明白了 - static inline 应该可以解决这个问题 - 不是吗?或者可能用 __LINE__ 替代 __COUNTER__ - Aconcagua
@Aconcagua - static inline 可能会有用。但是我不知道使用 inline 是否有意义。相对于什么计算 __LINE__,我不确定是否存在同样的问题,但我认为它是在所有包含完成后的当前TU中计算的。我可能错了。 - StoryTeller - Unslander Monica
test.c:1:#include <FileOf100Lines> 2:void f(){log(“something”);} 如果现在__LINE__在包含之后解析,那么日志系统如何有意义地使用它(打印line: 101而不是line: 1)? - Aconcagua
显示剩余2条评论

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和保证复制省略的效果,所有内容都很好地实现了高效,因此不需要担心。

在线演示


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接