以下是代码审查中发现的任务。我想根据特殊类型的比较谓词从集合中选择最小值。像这样:
struct Complex { ... };
float calcReduction(Complex elem);
Complex findMinValueWithPredicates(const std::vector<Complex>& values)
{
auto it = std::min_element(values.begin(), values.end(),
[](const Complex& a, const Complex& b) {
return calcReduction(a) < calcReduction(b);
});
if (it == values.end()) throw std::runtime_error("");
return *it;
}
在这里,我找到了基于谓词的最小元素。此谓词计算两个值的减少到float
,然后比较这些浮点数。工作正常,看起来整洁。
你能看到问题吗?是的,对于一组N
元素,calcReduction()
被称为2N
次,而仅计算N
次就足够了 - 每个元素一次。
解决此问题的一种方法是编写显式计算:
Complex findMinValueExplicit(const std::vector<Complex>& values)
{
float minReduction = std::numeric_limits<float>::max();
Complex minValue;
for (Complex value : values)
{
float reduction = calcReduction(value);
if (reduction < minReduction)
{
minReduction = reduction;
minValue = value;
}
}
if (minReduction == std::numeric_limits<float>::max()) throw std::runtime_error("");
return minValue;
}
它工作正常,我们只对N
进行了calcReduction()
次调用。但是,与明确调用min_element
相比,它看起来过于冗长,意图也不那么清晰。因为当你打电话给min_element
时,很容易猜到你会找到一个最小元素,你知道。
我现在唯一的想法是创建我自己的算法,如min_element_with_reduction
,接受范围和缩小功能。听起来很合理,但我想知道是否有任何现成的解决方案。
有关如何以明确的意图和一些现成解决方案解决此任务的任何想法?欢迎提升。 C ++ 17和范围很有趣。
答案 0 :(得分:6)
您可以使用boost::range
library。
auto reductionLambda = [](const Complex& a) { return calcReduction(a); };
auto it = boost::range::min_element(values | boost::adaptors::transformed(
std::ref(reductionLambda));
范围本身也应该使用C ++ 17来标准C ++。
修改强>
正如我们在评论中指出的那样,这也会使转换成两次。
所以这里有趣:
#include <boost/iterator/iterator_adaptor.hpp>
#include <boost/assign.hpp>
#include <algorithm>
#include <iostream>
#include <vector>
#include <functional>
template <class Iterator, class UnaryFunction>
class memoizing_transform_iterator
: public boost::iterator_adaptor<
memoizing_transform_iterator<Iterator, UnaryFunction> // Derived
, Iterator // Base
, std::decay_t<decltype(std::declval<UnaryFunction>()(std::declval<typename Iterator::value_type>()))> // Value
, boost::forward_traversal_tag // CategoryOrTraversal
>
{
public:
memoizing_transform_iterator() {}
explicit memoizing_transform_iterator(Iterator iter, UnaryFunction f)
: memoizing_transform_iterator::iterator_adaptor_(iter), fun(f) {}
static int total;
private:
friend class boost::iterator_core_access;
void increment() { ++this->base_reference(); memoized = false; }
using MemoType = std::decay_t<decltype(std::declval<UnaryFunction>()(std::declval<typename Iterator::value_type>()))>;
MemoType& dereference() const
{
if (!memoized) {
++total;
memoized = true;
memo = fun(*this->base());
}
return memo;
}
UnaryFunction fun;
mutable bool memoized = false;
mutable MemoType memo;
};
template <class Iterator, class UnaryFunction>
auto make_memoizing_transform_iterator(Iterator i, UnaryFunction&& f)
{
return memoizing_transform_iterator<Iterator, UnaryFunction>(i, f);
}
template<class I, class U>
int memoizing_transform_iterator<I, U>::total = 0;
// THIS IS COPIED FROM LIBSTDC++
template<typename _ForwardIterator>
_ForwardIterator
min_el(_ForwardIterator __first, _ForwardIterator __last)
{
if (__first == __last)
return __first;
_ForwardIterator __result = __first;
while (++__first != __last)
if (*__first < *__result)
__result = __first;
return __result;
}
int main(int argc, const char* argv[])
{
using namespace boost::assign;
std::vector<int> input;
input += 2,3,4,1,5,6,7,8,9,10;
auto transformLambda = [](const int& a) { return a*2; };
auto begin_it = make_memoizing_transform_iterator(input.begin(), std::ref(transformLambda));
auto end_it = make_memoizing_transform_iterator(input.end(), std::ref(transformLambda));
std::cout << *min_el(begin_it, end_it).base() << "\n";
std::cout <<begin_it.total;
return 0;
}
基本上我实现了一个迭代器,它记住了调用转换仿函数的结果。但奇怪的是,至少在在线编译器中,迭代器在它们的解除引用值被比较之前被复制(从而破坏了记忆的目的)。但是,当我只是从libstdc ++复制实现时,它按预期工作。也许你可以在真机上尝试一下?实例是here。
小编辑:
我在VS2015上进行了测试,它与std::min_element
按预期工作。
答案 1 :(得分:3)
唯一缺少的是元迭代器。
元迭代器接受一个迭代器,并创建一个包含它的副本的迭代器。它将所有操作传递给包含的迭代器,除非dereferenced返回包含迭代器的副本。
无论如何,用于此的代码也可用于创建一个超过size_t或int或类似torsor的迭代器 - 喜欢。
template<class It, class R>
struct reduced_t {
It it;
R r;
friend bool operator<( reduced_t const& lhs, reduced_t const& rhs ) {
return lhs.r < rhs.r;
}
};
template<class It, class F>
reduced_t<It, std::result_of_t<F(typename std::iterator_traits<It>::reference)>>
reducer( It it, F&& f ) {
return {it, std::forward<F>(f)(*it)};
}
template<class It, class F>
It reduce( It begin, It end, F&& f ) {
if (begin==end)
return begin;
return std::accumulate(
meta_iterator(std::next(begin)), meta_iterator(end),
reducer(begin, f),
[&](
auto&& reduced, // reduced_t<blah...> in C++11
It i
) {
auto r2 = reducer( i, f );
return (std::min)(reduced, r2);
}
).it;
};
每个迭代器只调用一次 f(*it)
。
我不会称之为......显而易见。诀窍是我们使用accumulate
和元迭代器来实现min_element
,然后我们可以让accumulate
对转换后的元素进行操作(它被调用一次,然后返回)。
您可以使用基元在基于堆栈的编程风格中重写它,但是有很多基元要编写。也许发布范围-v3。
此时,我正在想象有一些高性能的组合编程库。如果我这样做,我们可以做到以下几点:
reducer( X, f )
可以使用graph( deref |then| f )(X)
进行重新定位order_by( get_n_t<1> )
进行排序。
accumulate
来电可以阅读accumulate( skip_first(range), g(begin(range)), get_least( order_by( get_n_t<1> ) ) )
。
不确定这是否更清楚。
答案 2 :(得分:3)
这是一个使用的解决方案(已经与range-v3 library一起使用,即即将推出的Ranges TS的作者实现)
#include <range/v3/all.hpp>
#include <iostream>
#include <limits>
using namespace ranges::v3;
int main()
{
auto const expensive = [](auto x) { static int n; std::cout << n++ << " "; return x; };
auto const v = view::closed_iota(1,10) | view::transform(expensive);
auto const m1 = *min_element(v);
std::cout << "\n" << m1 << "\n";
auto const inf = std::numeric_limits<int>::max();
auto const min = [](auto x, auto y) { return std::min(x, y); };
auto const m2 = accumulate(v, inf, min);
std::cout << "\n" << m2 << "\n";
}
带输出的
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
1
19 20 21 22 23 24 25 26 27 28
1
如您所见,使用min_element
进行2N
次比较,但只使用accumulate
N
。
答案 3 :(得分:2)
如果你将minElem作为lambda参数,你可以使用min_element
Complex findMinValueWithPredicates(const std::vector<Complex>& values)
{
float minElem = std::numeric_limits<float>::max();
auto it = std::min_element(values.begin(), values.end(),
[&minElem](const Complex& a, const Complex& b) {
float tmp = calcReduction(a);
if (tmp < minElem) {
minElem = tmp;
return true;
}
return false;
});
if (it == values.end()) throw std::runtime_error("");
return *it;
}
编辑:
为什么在不使用b
时这会起作用?
25.4.7.21 min_element
21返回:第一个迭代器i在[first,last]范围内 对于[first,last]范围内的每个迭代器j,以下内容 相应的条件成立:!(* j <* i)或comp(* j,* i)== false。 如果first == last,则返回last。
因为b
应该被命名为smallestYet
(代码来自cplusplus.com)
template <class ForwardIterator>
ForwardIterator min_element ( ForwardIterator first, ForwardIterator last )
{
if (first==last) return last;
ForwardIterator smallest = first;
while (++first!=last)
if (*first<*smallest) // or: if (comp(*first,*smallest)) for version (2)
smallest=first;
return smallest;
}
这引出了我最喜欢的一句话:
“计算机科学只有10个难题: 缓存失效,命名事物和逐个错误。“
b
。minElem
缓存可能不正确。b
本来应该更有意义,因为它是“取消引用最小元素的迭代器”或smallestYet
。答案 4 :(得分:2)
这是另一种选择,但它仍然是你的第二个解决方案。说实话,它看起来并不清楚,但有人可能会喜欢它。 (我使用std::pair<float, Complex>
来存储缩小结果和减少的值。)
std::pair<float, Complex> result{std::numeric_limits<float>::max(), {}};
auto output_function = [&result](std::pair<float, Complex> candidate) {
if (candidate.first < result.first)
result = candidate;
};
std::transform(values.begin(), values.end(),
boost::make_function_output_iterator(output_function),
[](Complex x) { return std::make_pair(calcReduction(x), x); });
P.S。如果您的calcReduction
费用很高,您是否考虑过Complex
个对象中的缓存结果?这将导致稍微复杂的实现,但您将能够使用简单std::min_element
,这使您的意图清晰。