【问题标题】:Nesting of subexpressions in expression templates表达式模板中子表达式的嵌套
【发布时间】:2019-06-16 14:15:56
【问题描述】:

我们正在编写一个表达式模板库来处理具有稀疏梯度向量(一阶自动微分)的值的操作。我试图弄清楚如何根据表达式是否是临时的,通过引用或值来嵌套子表达式。

我们有一个 Scalar 类,它包含一个值和一个稀疏梯度向量。我们使用表达式模板(如 Eigen)来防止构造和分配过多的临时 Scalar 对象。因此我们有类Scalar 继承自ScalarBase<Scalar> (CRTP)。

ScalarBase< Left >ScalarBase< Right > 类型的对象之间的二元运算(例如 +、*)返回一个继承自 ScalarBase< ScalarBinaryOp<Left, Right,BinaryOp> >ScalarBinaryOp<Left, Right,BinaryOp> 对象:

template< typename Left, typename Right >
ScalarBinaryOp< Left, Right, BinaryAdditionOp > operator+(
    const ScalarBase< Left >& left, const ScalarBase< Right >& right )
{
    return ScalarBinaryOp< Left, Right, BinaryAdditionOp >( static_cast< const Left& >( left ),
        static_cast< const Right& >( right ), BinaryAdditionOp{} );
}

ScalarBinaryOp 必须持有一个值或对LeftRight 类型的操作数对象的引用。持有者的类型由RefTypeSelector&lt; Expression &gt;::Type的模板特化定义。

目前这始终是一个 const 引用。它目前适用于我们的测试用例,但持有对临时子表达式的引用似乎不正确或不安全。

显然我们也不希望复制包含稀疏梯度向量的Scalar 对象。如果xyScalar,则表达式x+y 应该保持对xy 的常量引用。但是,如果f 是从ScalarScalar 的函数,则x+f(y) 应该持有对x 的常量引用和f(y) 的值。

因此我想传递有关子表达式是否是临时的信息。我可以将它添加到表达式类型参数中:

ScalarBinaryOp&lt; typename Left, typename Right, typename BinaryOp , bool LeftIsTemporary, bool RightIsTemporary &gt;

RefTypeSelector:

RefTypeSelector&lt; Expression, ExpressionIsTemporary &gt;::Type

但是我需要为每个二元运算符定义 4 种方法:

ScalarBinaryOp< Left, Right, BinaryAdditionOp, false, false > operator+(
    const ScalarBase< Left >& left, const ScalarBase< Right >& right );
ScalarBinaryOp< Left, Right, BinaryAdditionOp, false, true > operator+(
    const ScalarBase< Left >& left, ScalarBase< Right >&& right );
ScalarBinaryOp< Left, Right, BinaryAdditionOp, true, false > operator+(
    ScalarBase< Left >&& left, const ScalarBase< Right >& right );
ScalarBinaryOp< Left, Right, BinaryAdditionOp, true, true > operator+(
    ScalarBase< Left >&& left, ScalarBase< Right >&& right )

我希望能够通过完美的转发来实现这一点。但是我不知道如何在这里实现这一点。首先,我不能使用简单的“通用参考”,因为它们几乎可以匹配任何东西。我想可能将通用引用和 SFINAE 结合起来只允许某些参数类型,但我不确定这是要走的路。另外我想知道我是否可以对有关 Left 和 Right 最初是左值还是右值引用的信息进行编码,这些信息在 Left 和 Right 类型中参数化 ScalarBinaryOp 而不是使用 2 个额外的布尔参数以及如何检索该信息。

我必须支持 gcc 4.8.5,它主要符合 c++11。

2019/08/15 更新:实施

template < typename Expr >
class RefTypeSelector
{
   private:
   using Expr1 = typename std::decay<Expr>::type;
   public:
   using Type = typename std::conditional<std::is_lvalue_reference<Expr>::value, const Expr1&,Expr1>::type;
};
template< typename Left, typename Right, typename Op >
class ScalarBinaryOp : public ScalarBase< ScalarBinaryOp< Left, Right, Op > >
{

public:

    template <typename L, typename R>
    ScalarBinaryOp( L&& left, R&& right, const Op& op )
        : left_( std::forward<L>(left) )
        , right_( std::forward<R>(right) ))
        , ...
    {
    ...
    }

    ...

private:
    /** LHS expression */
    typename RefTypeSelector< Left >::Type left_;

    /** RHS expression */
    typename RefTypeSelector< Right >::Type right_;

...
}   

template< typename Left, typename Right,
typename Left1 = typename std::decay<Left>::type,
typename Right1 = typename std::decay<Right>::type,
typename std::enable_if<std::is_base_of<ScalarBase<Left1>, Left1>::value,int>::type = 0,
typename std::enable_if<std::is_base_of<ScalarBase<Right1>, Right1>::value,int>::type = 0 >
ScalarBinaryOp< Left, Right, BinaryAdditionOp > operator+(
    Left&& left, Right&& right )
{

    return ScalarBinaryOp< Left, Right, BinaryAdditionOp >( std::forward<Left>( left ),
        std::forward<Right>( right ), BinaryAdditionOp{} );
}

【问题讨论】:

    标签: c++ c++11 crtp perfect-forwarding expression-templates


    【解决方案1】:

    您可以将左值/右值信息编码为LeftRight 类型。例如:

    ScalarBinaryOp<Left&&, Right&&> operator+(
        ScalarBase<Left>&& left, ScalarBase<Right>&& right)
    {
        return ...;
    }
    

    ScalarBinaryOp 是这样的:

    template<class L, class R>
    struct ScalarBinaryOp
    {
        using Left = std::remove_reference_t<L>;
        using Right = std::remove_reference_t<R>;
    
        using My_left = std::conditional_t<
            std::is_rvalue_reference_v<L>, Left, const Left&>;
        using My_right = std::conditional_t<
            std::is_rvalue_reference_v<R>, Left, const Right&>;
    
        ...
    
        My_left left_;
        My_right right_;
    };
    

    或者,您可以显式地按值存储所有内容,Scalars 除外。为了能够按值存储Scalar,您可以使用包装类:

    x + Value_wrapper(f(y))
    

    包装很简单:

    struct Value_wrapper : Base<Value_wrapper> {
        Value_wrapper(Scalar&& scalar) : scalar_(std::move(scalar)) {}
    
        operator Scalar() const {
            return std::move(scalar_);
        }
    
        Scalar&& scalar_;
    };
    

    RefTypeSelector 专门用于Value_wrapper

    template<> struct RefTypeSelector<Value_wrapper> {
        using Type = Scalar;
    };
    

    二元运算符定义保持不变:

    template<class Left, class Right>
    ScalarBinaryOp<Left, Right> operator+(const Base<Left>& left, const Base<Right>& right) {
        return {static_cast<const Left&>(left), static_cast<const Right&>(right)};
    }
    

    完整示例:https://godbolt.org/z/sJ3NfG

    (我在上面使用了一些 C++17 功能只是为了简化符号。)

    【讨论】:

    • 谢谢。这回答了我问题的第二部分。但是我仍然需要为每个二进制操作编写 4 个方法,为每个一元操作编写 2 个方法。这可以为每个运算符编写一个模板方法吗?然而,由于语法原因,Value_wrapper 并不有趣,而且用户通常不知道 f(y) 是在标量还是标量上返回未计算的表达式。
    • @BernardGODARD,您要么编写 4 个方法,要么使用带有 SFINAE 的转发引用来检查 LeftRight 是否可转换为 Base&lt;Expr&gt; 对于某些 Expr。我在这里看不到其他选择。 Value_wrapper 也可以使用表达式。它的作用只是传递有关如何在表达式中存储参数的信息。
    • 谢谢。那我试试SFINAE。
    • @BernardGODARD,在您实施 SFINAE 并查看它在实践中的工作方式之后,请在此处留下一个小注释,说明这种方法是否可行。这是一个有趣的问题。注意:如果存储类型仅从值类别推断,一些看起来很无辜的代码,如auto s = x + y; return s + ...; 将导致对局部变量s 的悬空引用。
    • 您描述的情况只是返回表达式模板对象的方法的问题。大多数方法的返回类型通常不是表达式模板,而是评估对象(例如标量)。对于返回表达式模板的方法,作者负责确保表达式树中没有对局部变量的引用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-13
    • 2018-04-16
    • 1970-01-01
    • 2015-09-27
    • 1970-01-01
    相关资源
    最近更新 更多