防止在临时范围之外使用类

Prevent use of a class outside of temporary scope?

本文关键字:范围      更新时间:2023-10-16

有没有办法判断一个实例是否在临时范围内构造,或者防止它在临时范围外使用?我猜没有,但话说回来,我总是对C++超越其自身设计限制的能力感到惊讶。

我承认,这是一个有点奇怪的问题,除了提供背景故事之外,我不知道如何"证明"这种欲望。

这个问题源于我们用来将数量惊人的遗留系统粘合在一起的一个穿梭类,每个遗留系统都有自己的数据表示概念。举一个熟悉的例子,以字符串为例。我们可以用字符串的每个"样式"重载API中的每个方法:

void some_method(const char* arg);
void some_method(const std::string& arg);
void some_method(const QString& arg);
void some_method(const XmlDocString& arg);
void some_method(const wire_string& arg);

或者我们可以做:

void some_method(const StringArg& arg);

这个helper类在哪里(让我们暂时忽略字符串编码,为了这个问题的目的,只假设坏的旧C风格字符串):

class StringArg {
public:
  StringArg() : m_data(""), m_len(0) {}
  template<size_t N>
  StringArg(const char (&s)[N]) : m_data(s), m_len(N-1) {}
  StringArg(const char* s) : m_data(s?s:"") { m_len = strlen(m_data); }      
  template<class T>
  StringArg(const T& t) : m_data(data_from(t)), m_len(len_from(t)) {}
  const char* data() const { return m_data; }
  const char* size() const { return m_len; }
private:
  const char* m_data;
  size_t m_len;
};
const char* data_from(const std::string& s) { return s.c_str(); }
size_t len_from(const std::string& s) { return s.size(); }
template<class XmlType>
const char* data_from(const XmlString<XmlType>& s) { return &s.content()[0]; }
template<class XmlType>
size_t len_from(const XmlString<XmlType>& s) { return s.byte_length(); }

ADL选择各种data_from()/len-from()来为我们提供一个由其他东西及其大小支持的缓冲区。事实上,有额外的元数据来捕获关于缓冲区性质和如何迭代它的重要信息,但本次讨论的重要一点是StringArg在临时范围内使用,复制成本低,提供了对接口外部其他东西支持的缓冲区的快速访问,我们现在实际上不需要关心这些缓冲区的类型,参数检查或长度计算在边界处进行一次。

现在,有人可以自由地用两个截然不同的字符串类来调用它:

interface_method(header() + body.str() + tail(), document.read().toutf8());

我们不需要关心这里发生的事情的寿命或类型,在内部,我们可以传递指向那些缓冲区的指针,比如糖果,将它们切片,解析,一式三份记录,而不会意外分配或长时间的内存拷贝。只要我们从不挂在这些缓冲区上,在内部,这是安全快速的,维护起来很愉快。

但随着API的使用越来越广泛,StringArg(也许不足为奇)被用于临时范围之外的其他地方,就好像它是另一个字符串类一样,由此产生的火花令人印象深刻。考虑:

std::string t("hi");
write(StringArg(t+t)); //Yes.
StringArg doa(t+t); //NO!
write(doa); //Kaboom?

t+t创建一个临时对象,StringArg将指向该临时对象的内容。在临时范围内,这只是例行公事,没有什么有趣的。当然,在它之外,它是极其危险的。指向随机堆栈内存的挂起指针。当然,对write()的第二次调用实际上在大多数情况下都能很好地运行,尽管它显然是错误的,这使得检测这些错误变得非常困难。

我们来了。我想允许:

void foo(const StringArg& a);
foo(not_string_arg());
foo(t+t);

我想防止或检测:

StringArg a(t+t); //No good

如果以下几项也不可能,我也会很好,尽管这很好:

foo(StringArg(t+t)); //Meh

如果我能检测到这个东西正在构建的范围,我实际上可以去安排将内容复制到构造函数中的一个稳定缓冲区中,类似于std::string,或者在运行时抛出一个异常,或者更好的是,如果我能在编译时阻止它,以确保它只按设计使用。

不过,实际上,我只希望StringArg永远是方法参数的类型。最终用户将永远不必键入"StringArg"才能使用API。曾经你可能希望这很容易记录下来,但一旦一些代码看起来有效,它就会成倍地增加。。。

我试过让StringArg不可复制,但这没有多大帮助。我已经尝试创建一个额外的shuttle类和一个非常量引用,试图以我的方式伪造隐式转换。显式关键字似乎让我的问题变得更糟,促进了"StringArg"的键入。我试着摆弄一个带有部分专门化的额外结构,这是唯一知道如何构造StringArg的东西,并隐藏StringArg。。。类似于:

template<typename T> struct MakeStringArg {};
template<> struct MakeStringArg<std::string> { 
  MakeStringArg(const std::string& s); 
  operator StringArg() const;
}

因此,用户必须用MakeStringArg(t+t)、MakeFooArg(foo)和MakeBarArg(bar)包装所有参数。。。现有的代码不会编译,而且在任何情况下都会扼杀使用接口的乐趣。

在这一点上,我并没有超越宏观技巧。我的魔术袋现在看起来很空。有人有什么建议吗?

所以Matt McNabb指出

std::string t("hi");
const StringArg& a = t + t;

这会导致临时StringArg的生存时间比它所指向的内容长。我真正需要的是一种方法来确定构造StringArg时的完整表达式何时结束。这实际上是可行的:

class StringArg {
public:
  template<class T>
  StringArg(const T& t, const Dummy& dummy = Dummy()) 
    : m_t(content_from(t)), m_d(&dummy) {
    m_d->attach(this);
  }
  ~StringArg() { if (m_d) m_d->detach(); }
private:
  void stale() {
    m_t = ""; //Invalidate content
    m_d = NULL; //Don't access dummy anymore
    //Optionally assert here
  }
  class Dummy {
  public:
    Dummy() : inst(NULL) {}
    ~Dummy() { if (inst) inst->stale(); }
    void attach(StringArg* p) { inst = p; }
    void detach() { inst = NULL; }
    StringArg* inst;
  };
  friend class Dummy;
private:
  const char* m_t;
  Dummy* m_d;
};

这样,马特的例子和我希望阻止的所有其他例子都被挫败了:当完整的表达式结束时,StringArg不再指向任何可疑的东西,所以任何"给定名称"的StringArg都保证是无用的。

(如果不清楚为什么这样做,那是因为Dummy必须在使用它的StringArg之前构建,因此StringArg保证在Dummy之前被销毁,除非它的寿命大于构建它的完整表达式。)

我承认我没有读过你的整篇文章,但你们的要求似乎有冲突。一方面,您声明希望避免悬挂引用,但随后您写道:

write(StringArg(t+t)); //Yes.
StringArg doa(t+t); //NO!

如果您唯一关心的是避免悬挂引用,那么将"NO!"更改为"Yes",并且在这两种情况下都将临时值移到本地值中。构造函数是:

StringArg(std::string &&arg)
{
     this->the_arg = std::move(arg);
}

其中CCD_ 2是CCD_。

当字符串由右值构造时,可以让StringArg存储该字符串;如果字符串由左值构造,则可以保留对该字符串的引用。

如果你想要一个只能由右值对象使用的方法的类,你可以在C++11中的方法上使用右值限定符:

class only_rvalue
{
public:
    only_rvalue() = default;
    only_rvalue( const only_rvalue& ) = delete; 
    only_rvalue( only_rvalue&& ) = default;
    only_rvalue& operator=( const only_rvalue&& ) = delete;
    only_rvalue& operator=( only_rvalue&& ) = default;
    void foo() &&;
    void bar() &&;
    void quux() &&;
};
only_rvalue create();
int main()
{
    only_rvalue{}.foo(); //ok
    create().bar(); //ok
    only_rvalue lvalue;
    lvalue.foo(); //ERROR
}