有没有一种简单的方法来处理返回std::pair的函数?

33

C++11有一个函数std::minmax_element,它返回一对值。然而,处理和阅读这个函数相当困难,并且会产生一个额外的、后来无用的变量来污染作用域。

auto lhsMinmax = std::minmax_element(lhs.begin(), lhs.end());
int &lhsMin = *(lhsMinMax.first);
int &lhsMax = *(lhsMinmax.second);

有没有更好的方法来做这件事?比如说:

int lhsMin;
int lhsMax;
std::make_pair<int&, int&>(lhsMin, lhsMax).swap(
    std::minmax_element(lhs.begin(), lhs.end()));

4
还没有人提到C++17吗?https://skebanga.github.io/structured-bindings/ - xaxxon
2
一个问题:使用 minmax_element 相对于同时使用 min_elementmax_element 有什么效率优势吗? - Saurav Sahu
2
@xaxxon:C++17的特性不在C++11中。问题标记为C++11。因此,这是一个有趣的话题,但并不是实际解决方案。 - Lightness Races in Orbit
2
@AdamHunyadi 鉴于它是一个标准的模板库,你自动拥有所有算法的源代码。我的在/usr/include/c++/6.2.1/bits/stl_algo.h中。minmax_element只在其中迭代一次范围,因此,在GCC的实现中,它比单独执行min_elementmax_element更有效。 - The Vee
1
我认为你提出的“更好的……类似”的代码并不是更好的,仅仅因为它更长、有更多的元素,并且无谓地将定义和初始化分开。 - hyde
显示剩余4条评论
6个回答

32

使用C++17中的结构化绑定,您可以直接进行

auto [lhsMinIt, lhsMaxIt] = std::minmax_element(lhs.begin(), lhs.end());

2
不错!你能否在一行代码中从迭代器对中获取值对呢? - davmac
2
@AdamHunyadi:C++1z是C++17的“尚未正式存在”的名称,就像C++0x变成了C++11(晚了!)和C++1y变成了C++14一样。 - Lightness Races in Orbit
9
感谢您的点赞,因为这是一个有趣的讨论点,并且 将会 是一个好的解决方案,但实际上这并没有回答问题(问题是关于 C++11 的)。 - Lightness Races in Orbit
1
@rubenvb 我认为这归结于更简洁的语法,能够在一个语句中声明和赋值变量,以及推断变量类型的能力。 - davmac
3
@LightnessRacesinOrbit: “C++1z”之后的标准将是什么?“C++2{”吗? :-) - celtschk
显示剩余3条评论

23
为避免污染您的作用域,您可以将赋值语句包含在较小的作用域中:
int lhsMin, lhsMax;

{
    auto it = std::minmax_element(lhs.begin(), lhs.end());
    lhsMin = *it.first;
    lhsMax = *it.second;
}

或者,您可以使用lambda表达式

int lhsMin, lhsMax;

std::tie(lhsMin, lhsMax) = [&]{
    auto it = std::minmax_element(lhs.begin(), lhs.end());
    return std::make_tuple(*it.first, *it.second);
}();

3
需要注意的是,两者都不适用于像主题贴的第一部分那样分配引用。第二部分表述混乱,很难确定其是否也暗示了分配两个 int& - The Vee
使用封闭作用域和std::tie比使用lambda更易于阅读,我认为。 - ABu
为什么第一个示例的末尾要加分号? - Paul

13

这看起来足够常见,可以促使我们编写一个辅助函数:

template <class T, std::size_t...Idx>
auto deref_impl(T &&tuple, std::index_sequence<Idx...>) {
    return std::tuple<decltype(*std::get<Idx>(std::forward<T>(tuple)))...>(*std::get<Idx>(std::forward<T>(tuple))...);
}

template <class T>
auto deref(T &&tuple)
    -> decltype(deref_impl(std::forward<T>(tuple), std::make_index_sequence<std::tuple_size<std::remove_reference_t<T>>::value>{})) {
    return deref_impl(std::forward<T>(tuple), std::make_index_sequence<std::tuple_size<std::remove_reference_t<T>>::value>{});
}

// ...

int lhsMin;
int lhsMax;
std::tie(lhsMin,lhsMax) = deref(std::minmax_element(lhs.begin(), lhs.end()));

index_sequence 是 C++14 中的内容,但是可以在 C++11 中实现

注意:即使在 C++14 中,我仍然会将 deref 的返回类型中的 decltype 重复使用,以便 SFINAE 可以应用。

在 Coliru 上查看实时演示


无论如何,SFINAE不适用吗? - Lightness Races in Orbit
2
@ViktorSehr你是什么意思? std::tie返回一个std::tuple <T&>。但是,所有std::tuple元函数(例如std::tuple_size)也适用于std::pair - Quentin
1
@StoryTeller,它确实可以,但变量的类型必须与对中元素的类型匹配。问题在于OP想要迭代器所引用的值,而不是迭代器本身。您可以使用std::tie和绑定到对,但对包含错误的值。 - davmac
如果你关心lvalue元组,std::tuple_size会失败。它需要std::remove_reference[_t]。这与其他的deref答案有相同的问题,因为产生prvalue的迭代器将通过std::forward_as_tuple导致悬空引用。 - Xeo
@Xeo 很好地发现了问题。就我所知,这应该会被修复。 - Quentin
显示剩余5条评论

4

我会直接写出自己的版本minmax_element:

template <class Iter, class R = typename iterator_traits<Iter>::reference>
std::pair<R,R> deref_minmax(Iter first, Iter last)
{
    auto iters = std::minmax_element(first, last);
    return std::pair<R,R>{*iters.first, *iters.second};
}

那么它只是:

int lo, hi;
std::tie(lo, hi) = deref_minmax(lhs.begin(), lhs.end());

这会限制您仅使用元素的单个副本(对于 `int` 不是很重要),同时让您保持对实际容器中引用的访问。
在 C++17 中,为了好玩,我们可以编写一个通用的解引用器:
template <class Tuple>
auto deref(Tuple&& tup) {
    return std::apply([](auto... args) {
        return std::tuple <decltype(*args)...>(*args...);
    }, tup);
}

auto& [lo, hi] = deref(std::minmax_element(lhs.begin(), lhs.end()));

这里的lohi是对容器本身的引用。


你的 deref 函数可能导致未定义行为。如果迭代器(或者无论 args 最终是什么)返回 prvalues,那么你会得到悬空引用,因为 std::forward_as_tuple 会为它们返回一个 rvalue 引用。 - Xeo
@Xeo 这取决于调用者是否保留引用。 - Barry
2
错误。在 return std::forward_as_tuple(...); 点处会得到悬空引用,因为这些临时变量是在该作用域中创建的,并且在函数返回后立即失效。 - Xeo
@Xeo 啊,一开始误解了你的评论。现在应该已经修复了。 - Barry

2
在当前标准的修订版中,没有办法一次分配两个引用(references),如果这是你想要的。请注意,除了需要C++17和辅助模板的Barry之外,没有其他答案可以做到这点。
然而,如果您想要对最小值和最大值元素进行读写访问,为什么不直接使用minmax_element提供的迭代器呢?与使用引用相比,它很可能会生成相同的机器代码,至少如果你的lhs是一个连续容器,则如此,但在其他情况下也可能如此。
您需要更少地依赖于自动类型推导,例如:
decltype(lhs.begin()) lhsMinIt, lhsMaxIt;
std::tie(lhsMinIt, lhsMaxIt) = std::minmax_element(lhs.begin(), lhs.end());
/* now access your minimum and maximum as *lhsMinIt and *lhsMaxIt */

如果您知道lhs的类型将是标准容器之一,您可以使用更清晰的类型指示符decltype(lhs)::iterator

2

在C++14或更高版本中

template<class=void, std::size_t...Is>
auto indexer( std::index_sequence<Is...> ) {
  return [](auto&&f){
    return f( std::integral_constant<std::size_t, Is>{}... );
  };
}
template<std::size_t N>
auto indexer() {
  return indexer( std::make_index_sequence<N>{} );
}
template<class F>
auto fmap_over_tuple( F&& f ) {
  return [f=std::forward<F>(f)](auto&& tuple) {
    using Tuple = decltype(tuple);
    using Tuple_d = std::decay_t<Tuple>;
    auto index = indexer< std::tuple_size< Tuple_d >::value >();
    return index(
      [&f, &tuple](auto&&...Is) {
        using std::get;
        return std::make_tuple(
          f( get<Is>( std::forward<Tuple>(tuple) ) )...
        );
      }
    );
  };
}

所以fmap_over_tuple接受一个函数对象。它返回一个函数对象,当传入一个类似元组的对象时,会对元组中的每个元素调用函数对象并生成一个新的元组。
然后我们编写dereference_tuple:
auto dereference_tuple = fmap_over_tuple(
  [](auto&& e) { return *e; }
);

现在在C++17中我们这样做:

auto[Min, Max] = dereference_tuple( std::minmax_element(lhs.begin(), lhs.end() );

并且鲍勃是你的叔叔。

在C++11中,只需像以前一样做即可。足够简洁。

C++14实时示例


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接