有没有办法判断实例是否在临时范围内构建,或者防止实例在临时范围之外使用?我猜没有,但是再次,我总是对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);

该帮助器类在哪里(让我们暂时忽略字符串编码,并出于该问题的目的仅假设使用错误的旧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)外部的其他东西,我们现在实际上不需要关心它的类型,并且任何转换,参数检查或长度计算都在边界处完成一次。

这样我们就可以随意使用两个截然不同的字符串类来调用它:
interface_method(header() + body.str() + tail(), document.read().toutf8());

我们不需要关心生命周期或这里发生的事情的类型,并且在内部我们可以将指针传递给那些缓冲区(例如糖果),对其进行 slice ,解析,将它们一式三份记录,而无需意外分配或冗长的内存副本。只要我们从不内部使用这些缓冲区,就可以安全,快速地进行维护。

但是随着该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类和一个非const引用,以按照自己的方式伪造隐式转换。显式关键字似乎使我的问题更糟,从而促进了“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)来包装所有参数...现有代码无法编译,并且无论如何都使使用该接口(interface)的乐趣大打折扣。

在这一点上,我还没有超越宏观黑客。我的把戏现在看起来非常空虚。有人有什么建议吗?

最佳答案

因此,马特·麦克纳伯(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;
};

这样,Matt的示例以及我希望防止的所有其他示例都受到了挫败:当完整表达式结束时,不再有StringArg指向任何可疑对象,因此任何“给定名称”的StringArg都将毫无用处。

(如果尚不清楚它为什么起作用,那是因为必须在使用它的StringArg之前构造一个Dummy,因此可以保证StringArg在Dummy之前被销毁,除非其生存期大于构造它的完整表达式。 )

关于c++ - 防止在临时范围之外使用类?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/25540000/

10-13 08:25