以不同方式分派 r 值和 l 值并使用 sfinae 禁用一个选项

Posted

技术标签:

【中文标题】以不同方式分派 r 值和 l 值并使用 sfinae 禁用一个选项【英文标题】:Dispatching r-values and l-values differently and using sfinae to disable one option 【发布时间】:2017-06-14 10:43:59 【问题描述】:

我想实现一个函数drop_if。给定一个一元谓词和一个顺序容器,它返回一个相同类型的容器,其中仅包含原始容器中不满足谓词的元素。

如果输入容器是一个右值,它应该就地工作,否则创建一个副本。这是通过分派到namespace internal 中的适当版本来实现的。如果容器的value_type 不能被覆盖——例如std::pair<const int, int>——即使容器是一个右值,也应该禁用右值版本。

以下代码 works as expected 带有 clang 和当前版本的 gcc (>=6.3)。

#include <algorithm>
#include <iostream>
#include <iterator>
#include <type_traits>
#include <utility>
#include <vector>

namespace internal

    template <typename Pred, typename Container,
        typename = typename std::enable_if<
        std::is_assignable<
            typename Container::value_type&,
            typename Container::value_type>::value>::type>
    Container drop_if( Pred pred, Container&& xs )
    
        std::cout << "r-value" << std::endl;
        xs.erase( std::remove_if( std::begin( xs ), std::end( xs ), pred ), std::end( xs ) );
        return std::move( xs );
    

    template <typename Pred, typename Container>
    Container drop_if( Pred pred, const Container& xs )
    
        std::cout << "l-value" << std::endl;
        Container result;
        auto it = std::back_inserter( result );
        std::remove_copy_if( std::begin( xs ), std::end( xs ), it, pred );
        return result;
    
 // namespace internal

template <typename Pred, typename Container,
    typename Out = typename std::remove_reference<Container>::type>
    Out drop_if( Pred pred, Container&& xs )

    return std::move( internal::drop_if( pred, std::forward<decltype(xs)>( xs ) ) );


typedef std::pair<int, int> pair_t;
typedef std::vector<pair_t> vec_t;

bool sum_is_even( pair_t p )

    return (p.first + p.second) % 2 == 0;


typedef std::pair<const int, int> pair_c_t;
typedef std::vector<pair_c_t> vec_c_t;

bool sum_is_even_c( pair_c_t p)

    return (p.first + p.second) % 2 == 0;


int main()

    vec_c_t v_c;
    drop_if( sum_is_even_c, v_c ); // l-value
    drop_if( sum_is_even_c, vec_c_t() ); // l-value

    vec_t v;
    drop_if( sum_is_even, v ); // l-value
    drop_if( sum_is_even, vec_t() ); // r-value

但是,not compile 对 MSVC++ 和 GCC 6.2 有影响,因为它们对 std::is_assignable 的行为不正确:

using T = std::pair<const int, int>;
const auto ok = std::is_assignable<T&, T>::value;
// ok == true on GCC 6.2 and MSVC++

查看this question 和Library Defect Report 2729 的回复。

我希望它适用于不同的容器和不同种类的对象,例如std::vector&lt;double&gt;std::map&lt;int, std::string&gt;std::map 案例(使用different inserter)是我遇到value_typesstd::pair&lt;const T, U&gt; 问题的情况。

您是否知道如何将调度/sfinae 更改为也适用于 MSVC++(在我的情况下为版本 MSVC++ 2017 15.2 26430.6)和 GCC 6.2 以下版本?

【问题讨论】:

这看起来像Library Defect Report 2729,可能还没有在所有地方实现。 @BoPersson 是的,T.C.在他的answer 中也与此问题相关联到此问题中链接的问题。现在我正在寻找解决方法。 我要做的第一件事是使用标签调度。 internal::drop_iftrue_typefalse_type 作为其第一个参数。 ::drop_ifstd::is_assignable&lt;...&gt; 传递给 internal::drop_if。 MSVC 的 SFINAE 支持很糟糕,这里不需要 SFINAE,只需调度即可。然后我们可以直接攻击is_assignable问题。 (SFINAE 中的 decltype 使 MSVC 死亡;标签调度中的 decltype 不会) 还有return xs;——你需要在这里移动或前进。 @Yakk:为什么我需要更多地转发return xs;?这不是happening automatically吗?编辑:哦,我现在明白了。在这种情况下,这不是多余的。谢谢你。我相应地编辑了我的问题。另外,对不起,我不明白你的其他评论。你能说明一下你的意思吗? 【参考方案1】:

问题似乎是 MSVC 的 std::pair&lt;const T, U&gt;::operator= 未禁用 SFINAE。即使实例化它不起作用,它仍然存在。

所以当你检测它是否存在时,它就存在。如果你执行它,它会编译失败。

我们可以解决这个问题。但这是一种解决方法。

namespace internal

    template <typename Pred, typename Container>
    Container drop_if( std::true_type reuse_container, Pred pred, Container&& xs )
    
        std::cout << "r-value" << std::endl;
        xs.erase( std::remove_if( std::begin( xs ), std::end( xs ), pred ), std::end( xs ) );
        return std::forward<Container>( xs );
    

    template <typename Pred, typename Container>
    Container drop_if( std::false_type reuse_container, Pred pred, const Container& xs )
    
        std::cout << "l-value" << std::endl;
        Container result;
        auto it = std::back_inserter( result );
        std::remove_copy_if( std::begin( xs ), std::end( xs ), it, pred );
        return result;
    
 // namespace internal

template<bool b>
using bool_k = std::integral_constant<bool, b>;

template<class T>
struct can_self_assign 
    using type = std::is_assignable<T&, T>;
;

template<class T>
using can_self_assign_t = typename can_self_assign<T>::type;

template<class T0, class T1>
struct can_self_assign<std::pair<T0, T1>>

    enum  t0 = can_self_assign_t<T0>::value, t1 = can_self_assign_t<T1>::value, x = t0&&t1 ;
    using type = bool_k< x >;
;

template<>
struct can_self_assign<std::tuple<>>

    using type = bool_k< true >;
;
template<class T0, class...Ts>
struct can_self_assign<std::tuple<T0, Ts...>>

    using type = bool_k< can_self_assign_t<T0>::value && can_self_assign_t<std::tuple<Ts...>>::value >;
;


template <typename Pred, typename Container,
    typename Out = typename std::remove_reference<Container>::type>
    Out drop_if( Pred pred, Container&& xs )

    using dContainer = typename std::decay<Container>::type;
    using can_assign = can_self_assign_t<typename dContainer::value_type>;
    using cannot_reuse = std::is_lvalue_reference<Container>;

    using reuse = std::integral_constant<bool, can_assign::value && !cannot_reuse::value >;

    return internal::drop_if( reuse, pred, std::forward<Container>( xs ) );

live example 和 other live example。

我还将您的 SFINAE 调度更改为基于标签的调度。

具有缺陷operator= 禁用的其他类型也可能需要can_self_assign 特化。值得注意的例子可能包括tuple&lt;Ts...&gt;vector&lt;T,A&gt; 和类似的。

我不知道编译器何时以及是否需要 operator= “不存在”,如果它在 std 类型中不起作用;我记得std::vector 有一次不需要它,但我还记得有一个提案添加了这样的要求。

【讨论】:

这很好。非常感谢。不过有一个问题:使用return std::forward&lt;Container&gt;(xs); 代替return std::move&gt;(xs); 是否有特定原因? @tobais 政策;我从转发引用转发。目前它始终是一个右值,但理论上它不一定是。随意移动静态断言我们有一个右值。【参考方案2】:

您没有说明您使用的是什么版本的 Visual C++ 编译器,但您是否尝试过使用尾随返回语法?

换句话说,替换这个:

template <typename Pred, typename Container,
          typename Out = typename std::remove_reference<Container>::type>
Out drop_if( Pred pred, Container&& xs )

有这样的东西?

template <typename Pred, 
          typename Container>
auto drop_if(Pred pred, Container&& xs) -> std::remove_reference<Container>::type

Visual C++ 编译器已逐渐在 C++ 11/14/17 中添加功能。我一直在努力使模板 MPL 工作,并且不得不解决一些“应该”工作但没有工作的事情。

我承认这有点猜测,但你可以试一试。

【讨论】:

感谢您的建议。我将我的 MSVC++ 版本添加到我的问题中。可悲的是您的解决方案does not help either。顺便说一句,链接的测试。使用“Microsoft (R) C/C++ Optimizing Compiler Version 19.00.23506 for x86。” 很抱歉。听起来您正在使用 Visual Studio 2015。我认为那是具有该编译器版本的版本。 C++ 一致性在那个方面非常好。编辑——哦,你说这是 2017 年。那我真的很惊讶这对你来说是个问题。但很高兴你得到了解决方案。【参考方案3】:

如果您希望您的代码专门用于像元组这样的对象的容器,您可以这样做(残酷但在旧版本的 gcc 和 MSVC 上工作):

#include <algorithm>
#include <iostream>
#include <iterator>
#include <type_traits>
#include <utility>
#include <vector>

namespace internal

    template <class... Ts>
        int foo(Ts&&... ts);

    template <typename Pred, typename Container, std::size_t... Is>
    auto drop_if( Pred pred, Container&& xs, std::index_sequence<Is...>) -> decltype(foo(std::declval<typename std::tuple_element<Is, typename std::remove_reference<typename Container::value_type>::type>::type&>() = std::declval<typename std::tuple_element<Is, typename std::remove_reference<typename Container::value_type>::type>::type>()...),  xs)
    
        std::cout << "r-value" << std::endl;
        xs.erase( std::remove_if( std::begin( xs ), std::end( xs ), pred ), std::end( xs ) );
        return xs;
    

    template <typename Pred, typename Container, class I>
    Container drop_if( Pred pred, const Container& xs, I )
    
        std::cout << "l-value" << std::endl;
        Container result;
        auto it = std::back_inserter( result );
        std::remove_copy_if( std::begin( xs ), std::end( xs ), it, pred );
        return result;
    
 // namespace internal

template <typename Pred, typename Container,
    typename Out = typename std::remove_reference<Container>::type>
    Out drop_if( Pred pred, Container&& xs )

    return internal::drop_if( pred, std::forward<decltype(xs)>( xs ), std::make_index_sequence<std::tuple_size<typename Out::value_type>::value> );


typedef std::pair<int, int> pair_t;
typedef std::vector<pair_t> vec_t;

bool sum_is_even( pair_t p )

    return (p.first + p.second) % 2 == 0;


typedef std::pair<const int, int> pair_c_t;
typedef std::vector<pair_c_t> vec_c_t;

bool sum_is_even_c( pair_c_t p)

    return (p.first + p.second) % 2 == 0;


int main()

    vec_c_t v_c;
    drop_if( sum_is_even_c, v_c ); // l-value
    drop_if( sum_is_even_c, vec_c_t() ); // l-value

    vec_t v;
    drop_if( sum_is_even, v ); // l-value
    drop_if( sum_is_even, vec_t() ); // r-value

[live demo]

【讨论】:

谢谢,但我希望它适用于各种对象和基本类型。【参考方案4】:

很好的发现,在这些更改得到修复之前,我会针对这种极端情况提供我的解决方案。

namespace internal 
template <typename T>
struct my_is_pair 
  static constexpr bool value = false;
;

template <typename K, typename V>
struct my_is_pair<std::pair<K, V>> 
  static constexpr bool value = true;
;

template <typename T, typename U, typename TIsPair = void>
struct my_is_assignable : std::is_assignable<T, U> 
  using is_pair_type = std::false_type;
;

template <typename T, typename U>
struct my_is_assignable<T,
                        U,
                        typename std::
                          enable_if<my_is_pair<typename std::decay<T>::type>::value
                                    && my_is_pair<typename std::decay<U>::type>::value>::
                            type>
  : std::integral_constant<bool,
                           std::is_assignable<
                             typename std::remove_reference<T>::type::first_type&,
                             const typename std::remove_reference<U>::type::first_type&>::
                               value
                             && std::is_assignable<
                                  typename std::remove_reference<T>::type::second_type&,
                                  const typename std::remove_reference<U>::type::
                                    second_type&>::value> 
  using is_pair_type = std::true_type;
;

template <
  typename Pred,
  typename Container,
  typename = typename std::
    enable_if<my_is_assignable<
                typename std::remove_reference<Container>::type::value_type,
                typename std::remove_reference<Container>::type::value_type>::value
              && std::is_rvalue_reference<Container&&>::value>::type>
Container drop_if(Pred pred, Container&& xs) 
  std::cout << "r-value" << std::endl;
  xs.erase(std::remove_if(std::begin(xs), std::end(xs), pred), std::end(xs));
  return xs;


template <typename Pred, typename Container>
Container drop_if(Pred pred, const Container& xs) 
  std::cout << "l-value" << std::endl;
  Container result;
  auto it = std::back_inserter(result);
  std::remove_copy_if(std::begin(xs), std::end(xs), it, pred);
  return result;

 // namespace internal

template <typename Pred,
          typename Container,
          typename Out = typename std::remove_reference<Container>::type>
Out drop_if(Pred pred, Container&& xs) 
  return internal::drop_if(pred, std::forward<decltype(xs)>(xs));


typedef std::pair<int, int> pair_t;
typedef std::vector<pair_t> vec_t;

bool sum_is_even(pair_t p)  return (p.first + p.second) % 2 == 0; 

typedef std::pair<const int, int> pair_c_t;
typedef std::vector<pair_c_t> vec_c_t;

bool sum_is_even_c(pair_c_t p)  return (p.first + p.second) % 2 == 0; 

int main() 
  vec_c_t v_c;
  drop_if(sum_is_even_c, v_c); // l-value
  drop_if(sum_is_even_c, vec_c_t()); // r-value

  vec_t v;
  drop_if(sum_is_even, v); // l-value
  drop_if(sum_is_even, vec_t()); // r-value

这只是为配对类型引入了is_assignable 特化,正如the defect report 中所建议的那样。我添加的另一件事是std::is_rvalue_reference,以防止对左值引用的调用(在您的解决方案中,它被Container::value_type 中的失败替换禁用,当Containervector&lt;...&gt;&amp; 时失败。

【讨论】:

以上是关于以不同方式分派 r 值和 l 值并使用 sfinae 禁用一个选项的主要内容,如果未能解决你的问题,请参考以下文章

SFINAE 在类型和非类型模板参数的情况下工作方式不同

使用不同的返回类型强制SFINAE

[POI2014]Couriers

r值和l值之间的差异[重复]

f检验的p值和r值是啥意思?

类模板构造函数中的 SFINAE