I like Mooing Duck's answer, but I thought a tiny tweak to handle an empty list would be nice. Although nothing wrong with disallowing that situation by contract, plenty of functions in the standard library impose constraints by contract.
But to answer your question...
Is there a more convenient way to get to the last element of a forward traversal range?
...no, there is not a more convenient way in general. You need to walk the forward list to get to the back most element. (Assuming you can't cache an iterator to the back most element.)
#include <forward_list>
#include <iostream>
#include <iterator>
#include <utility>
#include<cassert>
template <typename I>
auto back(I it, I last) {
using std::next;
auto n = it;
while (n != last) {
it = std::exchange(n, next(n));
}
return it;
}
// Container-to-iterator-pair convenience function.
template <typename C>
auto back(C const& c) {
using std::begin;
using std::end;
return back(begin(c), end(c));
}
static
auto operator<<(std::ostream& out, std::forward_list<int> const& l) -> std::ostream& {
auto sep = "";
for (auto&& x : l) {
out << std::exchange(sep, " ") << x;
}
return out;
}
template <typename C, typename I>
struct print {
C const& container;
I const& it;
print(C const& c, I const& i) : container{c}, it{i} {}
bool valid() const {
using std::begin;
using std::end;
using std::next;
auto b = begin(container);
auto e = end(container);
while(b != e) {
if (it == b) return true;
b = next(b);
}
return it == e;
}
template <typename CC, typename II>
friend auto operator<<(std::ostream& out, print<CC, II> const& p) -> std::ostream& {
using std::end;
assert(p.valid());
if (end(p.container) == p.it) {
return out << "(end)";
}
return out << *p.it;
}
};
print(struct allow_ctad_t, struct allow2_ctad_t)->print<void, void>;
int main() {
std::forward_list<int> const list{1, 2, 3, 4, 5};
std::cout << list << "\n";
auto it = back(list);
std::cout << print(list, it) << "\n";
std::cout << print(list, list.end()) << "\n";
}