使用表达式模板时,根据lvalue / rvalue提供类的不同实现

时间:2013-07-22 20:24:06

标签: c++ c++11 auto expression-templates

问题

假设我们实现了一个代表字符串的string类。然后,我们想要添加一个operator+来连接两个string,并决定通过表达式模板实现该功能,以避免在执行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;

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

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 result = str1 + str2 + ... + strN; // guess what's going on here?

问题

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

因此,我非常喜欢检测我是否正在构建rvalue或左值的方法,并且提供表达式模板的不同实现,具体取决于是否构建了右值(保持引用)或构建了左值(制作副本)。

是否有一个已建立的设计模式来处理这种情况?

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

  1. 可以根据this作为左值或右值来重载成员函数,即

    class C {
        void f() &; 
        void f() &&; // called on temporaries
    }
    
    然而,似乎我也不能在构造函数上做到这一点。

  2. 在C ++中,人们不能真正做``type overloads'',即提供相同类型的多个实现,具体取决于类型将如何使用(创建为左值或右值的实例)。

5 个答案:

答案 0 :(得分:13)

我在评论中开始这个,但它有点大。那么,让我们回答一下(尽管它并没有真正回答你的问题)。

这是auto的已知问题。例如,Herb Sutter here和Motti Lanzkron here更详细地讨论过它。

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

operator string() const;

如你所说,人们会提供

string operator auto() const;

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

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

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

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

答案 1 :(得分:2)

阐述comment I made to the OP;例如:

这只解决了分配对象或绑定到引用以及之后转换为目标类型的问题。这不是一个全面解决问题的方法(也见Yakkresponse to my comment),但它可以防止OP中出现的情况,并且通常更难编写这种容易出错的代码。

编辑:可能无法为类模板扩展此方法(更具体地说,std::move的特化)。 Macro'ing可以解决这个特定问题,但显然很难看。重载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{};
}

答案 2 :(得分:0)

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

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

不会强制编译错误?

答案 3 :(得分: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) {}
//...

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

希望部分是因为如果传递左值但是在stringbuilder完成其作业之前销毁了对象,则没有任何阻塞误用。

答案 4 :(得分: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来简化类型特征。