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

时间:2014-08-28 02:55:18

标签: c++ c++03

有没有办法判断一个实例是否已在临时范围内构建,或者是否阻止它在临时范围之外使用?我猜不到,但是再一次,我总是对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_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成为方法参数的类型。最终用户永远不必输入&#34; StringArg&#34;为了使用API​​。永远。您希望能够轻松记录文档,但是一旦某些代码看起来有效,它就会倍增,并且会倍增......

我尝试过使StringArg不可复制,但这并没有多大帮助。我已经尝试创建一个额外的穿梭类和一个非const引用来尝试以这样的方式伪造隐式转换。显式关键字似乎使我的问题变得更糟,促进&#34; StringArg&#34;的输入。我试着搞一个带有局部特化的额外结构,这是唯一知道如何构造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)包装所有参数...现有代码无法编译,无论如何它都会让人满意使用界面。

此时我还没有超越宏观攻击。我的包包技巧现在看起来很空洞。有人有什么建议吗?

3 个答案:

答案 0 :(得分:1)

所以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“给出一个名字”都保证是无用的。 / p>

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

答案 1 :(得分:0)

我承认我没有阅读你的整个帖子,但你似乎有相互矛盾的要求。一方面你声明你想避免悬挂引用,但是你写下:

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

如果你唯一关心的是避免悬挂引用,那么改变“不!”到“是”,并且在两种情况下都从临时值移出到本地值。构造函数将是:

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

其中the_argstd::string

当从rvalue构造字符串时,您可以StringArg存储字符串,如果字符串是从左值构造的,则保留对字符串的引用。

答案 2 :(得分:0)

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

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
}