防止返回值引用的复制构造和赋值操作

4

如果我有一个返回一个类的实例引用的函数,但是这个类的源代码不在我的控制范围内,比如说list<int>

list<int>& f();

我希望确保它的值分配给另一个引用,例如:

list<int> &a_list = f();

如果用户改为执行以下操作:
list<int> a_list = f(); // note: no '&', so the list is copied

我希望它成为编译时错误,因为用户只会操作列表的副本而不是原始列表(这从来不是我的应用程序想要的结果)。

有没有办法防止上述情况中的复制构造和赋值(比如通过某种“包装器”类)?

理想情况下,如果使用某个包装器类,比如wrapper<T>,我希望它适用于任何类型的对象T


是的,我知道对于我可以控制的类,我可以简单地将复制构造函数和赋值运算符设置为private,例如:

class MyClass {
public:
    // ...
private:
    MyClass( MyClass const& );
    MyClass operator=( MyClass const& );
};

禁止复制构造函数和赋值操作符;但是,如上所示,我想对于例如std::list这样的情况进行此操作,而我不能简单地将复制构造函数和赋值操作符设置为private


1
@GMan:当然,这只是一个1个字符的错误,所以它很琐碎;但是,很容易忘记输入'&'。如果你仍然不明白重点在哪里,那就直接忽略这个问题,继续前进吧。 - Paul J. Lucas
@Paul:但这就像是说你需要一种防止在本意为 x += 1 的情况下误写成 x + 1 的方法。这会带来很多痛苦,而不是试图“拯救”你,而是“你需要理解引用”。 - GManNickG
为了防止用户执行不应该执行的操作(正如我最初所述),可以使用@fingerprint211b:。如果用户在列表的副本上操作,那么显然他/她只对将在作用域结束时被销毁的副本进行更改。 - Paul J. Lucas
1
@GMan:我不同意你的建议。这不是关于“学习如何编程”。例如,auto_ptr<T>的创建是为了帮助用户防止意外地做错事情。在C++中,您可以完全正常地进行编程而不使用auto_ptr<T>,但您必须更加小心。我对您的评论最大的问题是,您认为只因为有人简单地忘记输入“&”就“不知道如何编程”。那很傲慢和自以为是。FYI。 - Paul J. Lucas
@Paul:使用智能指针是完全不同的,它是一种更简单的管理内存的方式,专注于表达而不是冗余。相反,这是一种防止意外复制的支撑;两者不可比较。忘记按键是正常的,但很容易修复(哦,它没有被修改,让我们看看...哦,它不是一个引用,修复了)。而且这种情况并不经常发生。我不确定你是否真的想要为了防止这种错误而使你的代码变得晦涩难懂。如果我听起来很傲慢,那我很抱歉,但记住使用引用确实是 C++ 的核心。你不应该帮助用户使用它。 - GManNickG
显示剩余4条评论
5个回答

2

鉴于问题已经明确,这是我的另一个答案。我的上一个答案可能对一些有着正常需求的人有用,所以我将保持不变。

所需行为是不可能实现的:请求是能够通用地返回类似于T&但不像T行为的东西。必须以某种方式向用户(和编译器!)表明,这个返回的东西实际上并不是引用。


1

我不相信语言中有任何可以让你这样做的东西。可以说,你可以返回一个指针,这样他们就必须采取明确的操作来复制它。

另一方面,这里有一个你可以使用的包装器的例子。请注意,Uncopyable是通过值而不是引用返回的。(但这没关系,因为它可能只有指针大小。)

#include <iostream>
#include <list>

template <typename T>
class Uncopyable
{
public:
    Uncopyable(T& r) : ref(r) {}

    T* operator->() { return &ref; }

private:
    T& ref;
};

Uncopyable<std::list<int> > get_list()
{
    static std::list<int> l;
    l.push_back(l.size());
    return l;
}

int main() 
{
    for (int i = 0; i < 10; ++i)
    {
        Uncopyable<std::list<int> > my_l = get_list();
        std::cout << my_l->size() << std::endl;
    }
}

问题在于用户必须知道 Uncopyable 并在他们的代码中使用它。理想情况下,我希望解决方案是透明的。 - Paul J. Lucas
这本身就是一个答案。我只是想确保没有聪明的方法来做我想做的事情。 - Paul J. Lucas
2
请查看我在帖子顶部的评论;如果您非常需要这种行为,请使用指针。您所要求的是将引用视为不是它所引用的东西,这正是它存在的目的。 - pkh

1

我认为这是一个奇怪的请求。通常,将列表复制或将其作为参考进行处理的策略应由用户决定,但如果出于某种原因,永远不正确地复制列表,则包装类可以解决问题。

如果用户知道自己在做什么,他应该了解使用列表的深复制与使用浅复制并修改原始列表之间的差异。如果用户不理解这一点,他就不适合使用C++。

[编辑]我刚刚注意到有人发布了几乎相同的解决方案。该类可以非常简单:

template <class T>
class NoncopyablePtr
{
private:
    T* ptr;

public:
    /*implicit*/ NoncopyablePtr(T* iptr): ptr(iptr) {}

    T* operator->() const
    {
        return ptr;
    }
};

这会使用户很难复制指针。他们必须显式调用operator->并解引用结果。现在只需返回NoncopyablePtr< std::list< int > >,您将使客户端非常难以(但不是不可能)复制该列表。

如果您不喜欢使用operator->,那么恐怕没有其他方法可以防止用户轻松地复制结果。


1

嗯,你可以用一个包装器来实现。为你的列表创建一个包装器,重载->但从不提供对真实引用的访问。我想可能有方法绕过这样的方法,但必须是故意的。应该足以告诉客户他们真的不应该这样做。


如果用户必须使用operator->,那么他/她不能像我上面展示的那样简单地编写正确的形式。 - Paul J. Lucas
@Paul:在任何可以(并且被允许)使用list<int>&的地方,他们都可以删除该引用。你必须更改语法或保留原始“问题”。 - GManNickG
“正确的形式”?这到底是什么意思?如果你坚持要一个不可能的答案,那么恐怕你只能祈求上帝改变宇宙了。我试图为你提供一些可以解决你所面临问题的东西。如果这还不够好……那么,祝你好运吧。 - Edward Strange
“正确的形式” 意味着可以将其写成 “正常的形式”:list<int> &a_list = f();。 - Paul J. Lucas

1
您可以从这个类继承,并创建一个匹配的构造函数来调用父类的构造函数(例如,具有相同数据构造函数,只需将数据传递给父类),并使复制构造函数和复制赋值私有。或者,您可以从boost::noncopyable和该类派生。然后,您可以安全地使用指向/引用基类的指针/引用。
编辑:如果该类具有接口,则可以创建一个实现该接口的装饰器,该装饰器不可复制,并且不提供获取其包装对象的引用/指针的方法。
如果这两种方法都不可行,您可以编写一个具有完全相同方法和外观的类,没有复制构造函数和复制赋值,它将调用您正在保护的类的适当方法(再次使用装饰器,但是这种方式比较困难)。但我会避免这样做。

根据我上面添加的文本,不可能从任意类T继承并具有匹配的构造函数。 - Paul J. Lucas
你使用这种方法能做到的最好的事情就是用默认构造函数捕获所有内容。但考虑到你的其他要求,这可能是你最好的选择。 - pkh
@pkh:我不明白你所说的“用默认构造函数捕获所有内容”的意思。也许你可以写一些代码来解释一下? - Paul J. Lucas
我的意思是,这个答案中描述的解决方案适用于任何具有0参数构造函数的类。 “默认”是一个不正确的词选择。 - pkh

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