在使用表达式模板时,根据左值/右值提供类的不同实现

Providing different implementations of a class depending on lvalue/rvalue when using expression templates

本文关键字:实现 表达式      更新时间:2023-10-16

问题

假设我们实现了一个string类,它表示字符串。然后,我们希望添加一个连接两个stringoperator+,并决定通过表达式模板实现,以避免在执行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使用了它

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

问题

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

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

是否有现成的设计模式来处理这种情况

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

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

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

    然而,我似乎也不能在构造函数上做到这一点。

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

我在一个注释中开始了这个,但它有点大。然后,让我们把它作为一个答案(即使它并不能真正回答你的问题)。

这是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可能。。。

详述我对OP的评论;示例:

这只解决了分配给对象或绑定到引用,然后转换为目标类型的问题。这并不是一个全面的解决问题的方法(也可以参见Yakk对我的评论的回应),但它阻止了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{};
}

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

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

不会强制编译错误吗?

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

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) {}
//...

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

hopping部分是因为如果传递了一个左值,但在字符串生成器完成其工作之前对象就被破坏了,那么就没有什么可以阻止误用的。

这里是解决悬挂引用问题的另一种尝试。不过,它并不能解决引用已修改内容的问题。

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

// 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 << "RVREFn";
        auto r = c + f() + "toto";
        std::cout << r << "n";
        std::string s (r);
        std::cout << s << "n";
    }
    {
        std::cout << "nnLVREFn";
        std::string str="lvref";
        auto r = c + str + "toto";
        std::cout << r << "n";
        std::string s (r);
        std::cout << s << "n";
    }
    {
        std::cout << "nnCLVREFn";
        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,它们不是困难所在。

附言:我使用C++14只是为了简化类型特征。