20

问题

假设我们实现了一个string表示,嗯,字符串的类。然后我们想要添加一个operator+连接两个strings 的 which ,并决定通过表达式模板来实现它以避免在做str1 + str2 + ... + strN.

运算符将如下所示:

stringbuilder<string, string> operator+(const string &a, const string &b)

stringbuilder是一个模板类,它又重载operator+并具有隐式string转换运算符。几乎是标准的教科书练习:

template<class T, class U> class stringbuilder;

template<> class stringbuilder<string, string> {
    stringbuilder(const string &a, const string &b) : a(a), b(b) {};
    const string &a;
    const string &b;
    operator string() const;
    // ...
}

// recursive case similar,
// building a stringbuilder<stringbuilder<...>, string>

只要有人这样做,上述实现就可以完美地工作

string result = str1 + str2 + ... + strN;

但是,它有一个微妙的错误。将结果分配给正确类型的变量将使该变量保存对构成表达式的所有字符串的引用。这意味着,例如,更改其中一个字符串将更改结果:

void print(string);
string str1 = "foo";
string str2 = "bar";
right_type result = str1 + str2;
str1 = "fie";
print(result); 

这将打印fiebar,因为 str1 引用存储在表达式模板中。情况变得更糟:

string f();
right_type result = str1 + f();
print(result); // kaboom

现在表达式模板将包含对已破坏值的引用,从而立即使您的程序崩溃

现在那是什么right_type?它当然是stringbuilder<stringbuilder<...>, string>,即表达式模板魔术为我们生成的类型。

现在为什么要使用这样的隐藏类型?事实上,人们并没有明确地使用它——但是 C++11 的 auto 可以!

auto result = str1 + str2 + ... + strN; // guess what's going on here?

问题

底线是:似乎一旦尝试存储表达式模板本身,这种实现表达式模板的方式(通过存储廉价引用而不是复制值或使用共享指针)就会被破坏。

因此,我非常喜欢一种检测我是在构建 rvalue 还是 lvalue 的方法,并根据是构建 rvalue(保留引用)还是构建 lvalue(制作副本)提供表达式模板的不同实现)。

是否有既定的设计模式来处理这种情况?

在我的研究过程中,我唯一能弄清楚的是

  1. 可以根据this是左值还是右值来重载成员函数,即

    class C {
        void f() &; 
        void f() &&; // called on temporaries
    }
    

    但是,似乎我也不能在构造函数上这样做。

  2. 在 C++ 中,不能真正做到“类型重载”,即提供同一类型的多个实现,这取决于类型将如何使用(创建为左值或右值的实例)。

4

5 回答 5

13

我在评论中开始了这个,但它有点大。然后,让我们让它成为一个答案(即使它并没有真正回答你的问题)。

这是auto. 例如,Herb Sutter在这里讨论过,Motti Lanzkron在这里讨论过更多细节。

正如他们所说,委员会中有讨论添加operator auto到 C++ 来解决这个问题。这个想法将不是(或除了)提供

operator string() const;

正如你所提到的,一个人会提供

string operator auto() const;

用于类型推导上下文。在这种情况下,

auto result = str1 + str2 + ... + strN;

不会将类型推断为result“正确类型”,而是推断类型,string因为这就是operator auto()返回的内容。

AFAICT 这在 C++14 中不会发生。C ++ 17也许...

于 2013-07-22T20:57:36.543 回答
2

详细说明我对 OP 的评论;例子:

这只解决了分配给对象或绑定到引用然后转换为目标类型的问题。它不是一个全面解决问题的方法(另见Yakk对我的评论的回应),但它阻止了 OP 中呈现的场景,并且通常更难编写这种容易出错的代码。

编辑:可能无法为类模板扩展这种方法(更具体地说,是 的专业化std::move)。宏化可以解决这个特定问题,但显然很难看。重载std::move将依赖于UB。

#include <utility>
#include <cassert>

// your stringbuilder class
struct wup
{
    // only use member functions with rvalue-ref-qualifier
    // this way, no lvalues of this class can be used
    operator int() &&
    {
        return 42;
    }
};

// specialize `std::move` to "prevent" from converting lvalues to rvalue refs
// (make it much harder and more explicit)
namespace std
{
    template<> wup&& move(wup&) noexcept
    {
        assert(false && "Do not use `auto` with this expression!");
    }
    // alternatively: no function body -> linker error
}

int main()
{
    auto obj = wup{};
    auto& lref = obj;
    auto const& clref = wup{};
    auto&& rref = wup{};

    // fail because of conversion operator
      int iObj = obj;
      int iLref = lref;
      int iClref = clref;
      int iRref = rref;
      int iClref_mv = std::move(clref);

    // assert because of move specialization
      int iObj_mv = std::move(obj);
      int iLref_mv = std::move(lref);
      int iRref_mv = std::move(rref);

    // works
    int i = wup{};
}
于 2013-07-22T21:54:21.037 回答
0

只是一个疯狂的想法(没有尝试过):

template<class T, class U>
class stringbuilder
{
  stringbuilder(stringbuilder const &) = delete;
}

不会强制编译错误?

于 2013-07-22T21:51:33.847 回答
0

一种可能的方法是使用空对象模式。虽然它可能会使您的字符串生成器更大,但它仍然会避免内存分配。

template <>
class stringbuilder<std::string,std::string> {
   std::string        lhs_value;
   std::string        rhs_value;
   const std::string& lhs;
   const std::string& rhs;

   stringbuilder(const std::string &lhs, const std::string &rhs) 
      : lhs(lhs), rhs(rhs) {}

   stringbuilder(std::string&& lhs, const std::string &rhs) 
      : lhs_value(std::move(lhs)), lhs(lhs_value), rhs(rhs) {}

   stringbuilder(const std::string& lhs, std::string&& rhs)
      : rhs_value(std::move(rhs)), lhs(lhs), rhs(rhs_value) {}

   stringbuilder(std::string&& lhs, std::string&& rhs)
      : lhs_value(std::move(lhs)), rhs_value(std::move(rhs)),
        lhs(lhs_value), rhs(rhs_value) {}
//...

如果构造函数的参数是左值,则存储对真实对象的引用。如果构造函数的参数是右值,您可以将其移动到几乎没有成本的内部变量中(移动操作很便宜)并存储对该内部对象的引用。其余代码可以访问引用,知道(好吧,至少希望)该字符串仍然存在。

希望的部分是因为如果传递了左值但对象在字符串生成器完成其工作之前被销毁,则不会阻止滥用。

于 2013-07-22T21:55:22.437 回答
0

这是解决悬空引用问题的另一种尝试。它并没有解决对已修改内容的引用问题。

这个想法是将临时变量存储到值中,但要引用左值(我们可以期望在 之后继续存在;)。

// Temporary => store a copy
// Otherwise, store a reference
template <typename T>
using URefUnlessTemporary_t
= std::conditional_t<std::is_rvalue_reference<T&&>::value
,                    std::decay_t<T>
,                    T&&>
;

template <typename LHS, typename RHS>
struct StringExpression
{
    StringExpression(StringExpression const&) = delete;
    StringExpression(StringExpression     &&) = default;

    constexpr StringExpression(LHS && lhs_, RHS && rhs_)
        : lhs(std::forward<LHS>(lhs_))
        , rhs(std::forward<RHS>(rhs_))
        { }

    explicit operator std::string() const
    {
        auto const len = size(*this);
        std::string res;
        res.reserve(len);
        append(res, *this);
        return res;
    }

    friend constexpr std::size_t size(StringExpression const& se)
    {
        return size(se.lhs) + size(se.rhs);
    }


    friend void append(std::string & s, StringExpression const& se)
    {
        append(s, se.lhs);
        append(s, se.rhs);
    }

    friend std::ostream & operator<<(std::ostream & os, const StringExpression & se)
    { return os << se.lhs << se.rhs; }

private:
    URefUnlessTemporary_t<LHS> lhs;
    URefUnlessTemporary_t<RHS> rhs;
};

template <typename LHS, typename RHS>
StringExpression<LHS&&,RHS&&> operator+(LHS && lhs, RHS && rhs)
{
    return StringExpression<LHS&&,RHS&&>{std::forward<LHS>(lhs), std::forward<RHS>(rhs) };
}

我毫不怀疑这可以简化。

int main ()
{
    constexpr static auto c = exp::concatenator{};
    {
        std::cout << "RVREF\n";
        auto r = c + f() + "toto";
        std::cout << r << "\n";
        std::string s (r);
        std::cout << s << "\n";
    }

    {
        std::cout << "\n\nLVREF\n";
        std::string str="lvref";
        auto r = c + str + "toto";
        std::cout << r << "\n";
        std::string s (r);
        std::cout << s << "\n";
    }

    {
        std::cout << "\n\nCLVREF\n";
        std::string const str="clvref";
        auto r = c + str + "toto";
        std::cout << r << "\n";
        std::string s (r);
        std::cout << s << "\n";
    }
}

注意:我不提供size()append()也不concatenator,它们不是困难所在。

PS:我只使用 C++14 来简化类型特征。

于 2016-03-30T13:40:36.263 回答