是否可以编写一个可以独占存储左值引用或对象的类?

Is it possible to write a class that can store lvalue references or objects exlusively?

提问人:TwistedBlizzard 提问时间:11/11/2023 最后编辑:TwistedBlizzard 更新时间:11/11/2023 访问量:128

问:

我厌倦了按值传递我的所有对象,然后移动,或者重载左值和右值。我正在尝试编写一个类,如果将右值传递到构造函数中,该类可以存储左值引用或普通对象。

这是骨架:

template<typename T>
class ref {
private:
    T* m_ptr = nullptr;
    std::optional<T> m_obj;
    bool is_lvalue = true;
public:
    ref(T& lvalue_ref) : m_ptr{ &lvalue_ref } {}
    ref(T&& rvalue_ref) : m_obj{ std::move(rvalue_ref) }, is_lvalue{ false } {}

    T& get() {
        return (is_lvalue) ? *m_ptr : *m_obj;
    }
};

我想将这个类与这样的函数一起使用,因此任何函数都不会进行任何复制。不幸的是,每当您获取值时,您都必须检查是否存储了引用/对象,并且默认构造其对象,即使我们存储左值,我们也会为此付费。是否有可能创建一个存储左值引用或对象的类,并且仍然能够让函数能够将 T 的左值和右值绑定到它?void foo(ref<bar> r) {}ref<T>std::optionalref<T>

C++ 右值引用

评论

2赞 user12002570 11/11/2023
引用不是对象。它不能“存储”在另一个对象中,如 、 等。vectorarrayclass
2赞 Pepijn Kramer 11/11/2023
或者有 std::reference_wrapper。但是,拥有这样的“方便”包装器可能会很棘手,某些对象会被隐式复制,使您无法跟踪对象的生命周期。在大型项目中,可能会带来惊喜。
1赞 Pepijn Kramer 11/11/2023
@TwistedBlizzard我的意思是用reference_wrapper来代替m_ptr。但如前所述,我认为这不是一个好主意。
2赞 user7860670 11/11/2023
左值和右值的重载通常涉及编写不同的逻辑。一个不透明的类可能存储一个对象或引用一个存储在其他地方的对象,这给生存期管理带来了麻烦。¿你打算如何使用这样的类?这个问题似乎是一个 XY 问题。
4赞 JaMiT 11/11/2023
这感觉像是一个 XY 问题。类型的参数可以绑定到两者,而无需重载。如果参数必须是非常量,那么通常使用右值引用是逻辑错误,或者需要不同的实现(即重载)。这不是 100% 的时间,但“厌倦”剩余的情况让我怀疑(不是 100% 确定)是否将错误的解决方案 (Y) 应用于实际问题 (X)。const T&T&T&&

答:

0赞 Enlico 11/11/2023 #1

我将首先引用我的评论,以便每个人都可以全面了解我的...视图。

我确信这是一个 XY 问题@JaMiT。事实上,这个问题的特点是针对一个定义不明确的问题的可能解决方案的骨架:你写道“我厌倦了按值传递我的所有对象,然后移动,或者重载左值和右值”。如何?展示一个代码示例,该示例使您认为需要解决问题。重载 rvalue ref 和 lvalue ref 的上下文是否为泛型上下文?或者你指的是采用具体值的函数?Q 中没有解释这些方面。

但我仍然认为可以尝试一些猜测。

我厌倦了按值传递我的所有对象,然后移动,或者重载左值和右值。

想要将这个类与 void 等函数一起使用,因此任何函数都不会进行任何复制。foo(ref<bar> r) {}ref<T>

我假设您有许多函数按值获取参数,和/或许多函数重载为 和 ,在任何一种情况下,arg(s) 的类型都是具体的(否则,如果这些函数是模板化的,我希望至少在您的问题正文中提到转发引用)。&&&

为了避免复制,我认为你会去(或者你已经去)这样的解决方案,它对几个函数重复,你不仅复制了标头中的接口,

// foo.hpp
#pragma once
#include "Bar.hpp"
void foo(Bar&);
void foo(Bar&&);

还有实现文件中的业务逻辑:

// foo.cpp
#include "Bar.hpp"
void foo(Bar& x) {
    business_logic(x);
}
void foo(Bar&&) {
    business_logic(std::move(x));
}

典型的客户端代码如下所示,

// main.cpp
#include "foo.hpp"
#include "Bar.hpp"
int main()
{
    foo(Bar{3});
    Bar x{3};
    foo(x);
}

如果是这种情况,一种解决方案可能是将函数模板化并将它们所需的两个显式实例化一起嵌入 cpp 文件中

// foo.hpp
#pragma once
template<typename T>
void foo(T&&);
// foo.cpp
#include "Bar.hpp"
template<typename T>
void foo(T&& t) {
    business_logic(std::forward<T>(t));
}
template void foo(Bar&);
template void foo(Bar&&);

请注意,我的玩具示例在两种情况下的长度相同,但两者之间存在一些差异。在我提出的解决方案中,

  • RVALUES 和 LVALUES 只有一个共享实现,因此您不会重复业务逻辑;
  • 你可以只对其他函数进行参数,而在“手动”重载的情况下,你必须在一个重载中写入,而在另一个重载中什么都不写;std::forward<T>std::move
  • 测试的好处是使用更简单的类型进行练习,而不仅仅是实例化模拟类型。fooBarfooBarForTests