Some <ranges> nice things (C++20 only):
#include <ranges>
const auto maxval = std::ranges::max_element(objs, std::less<>{}, &ObjType::eval);
if (maxval != objs.cend())
doStuffWith(*maxval);
where ObjType is the type of the sequence elements. The last check could also be on the size of the container as maxval would certainly be a dereferencable iterator when the sequence isn't empty, e.g.
if (!objs.empty()) ; // ...
Note however that as @NathanOliver has pointed out, this invokes eval() 2N-2 times. Here is a custom template that would call eval() exactly N times:
#include <optional>
#include <functional>
#include <type_traits>
template <class Range, class Cmp = std::less<>, class Proj = std::identity>
auto maxValue(const Range& rng, Cmp pred = Cmp{}, Proj p = Proj{})
{
using std::begin;
using std::end;
using ValueType = std::remove_cvref_t<std::invoke_result_t<Proj,
decltype(*begin(rng))>>;
auto first = begin(rng);
const auto last = end(rng);
if (first == last)
return std::optional<ValueType>{};
auto result = std::invoke(p, *first);
for (++first; first != last; ++first)
result = std::max(std::invoke(p, *first), result, pred);
return std::optional{result};
}
It doesn't return an iterator, but the resulting value - wrapped into a std::optional in case the range is empty (then the result is std::nullopt). Usage for a type Test with member function Test::eval() would be like this:
const auto max = maxValue(myContainer, std::less<>{}, &Test::eval);
Second and third argument have sensible defaults, so for primitive types and the like they could be left out.