Skip to content

Commit 7013466

Browse files
committed
Enable fmt::join for uncopyable iterators
If iterator not copyable mutate the underlying iterator Notably std::ranges::basic_istream_view::__iterator Addresses issue (fmtlib#3802)
1 parent 16cec4f commit 7013466

File tree

2 files changed

+73
-4
lines changed

2 files changed

+73
-4
lines changed

include/fmt/ranges.h

+20-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# include <iterator>
1414
# include <tuple>
1515
# include <type_traits>
16+
# include <utility>
1617
#endif
1718

1819
#include "format.h"
@@ -611,7 +612,7 @@ struct join_view : detail::view {
611612
basic_string_view<Char> sep;
612613

613614
join_view(It b, Sentinel e, basic_string_view<Char> s)
614-
: begin(b), end(e), sep(s) {}
615+
: begin(std::move(b)), end(e), sep(s) {}
615616
};
616617

617618
template <typename It, typename Sentinel, typename Char>
@@ -631,10 +632,25 @@ struct formatter<join_view<It, Sentinel, Char>, Char> {
631632
return value_formatter_.parse(ctx);
632633
}
633634

634-
template <typename FormatContext>
635-
auto format(const join_view<It, Sentinel, Char>& value,
635+
template <typename FormatContext, typename Iter,
636+
FMT_ENABLE_IF(std::is_copy_constructible<Iter>::value)>
637+
auto format(const join_view<Iter, Sentinel, Char>& value,
636638
FormatContext& ctx) const -> decltype(ctx.out()) {
637639
auto it = value.begin;
640+
return do_format(value, ctx, it);
641+
}
642+
643+
template <typename FormatContext, typename Iter,
644+
FMT_ENABLE_IF(!std::is_copy_constructible<Iter>::value)>
645+
auto format(join_view<Iter, Sentinel, Char>& value, FormatContext& ctx) const
646+
-> decltype(ctx.out()) {
647+
return do_format(value, ctx, value.begin);
648+
}
649+
650+
private:
651+
template <typename FormatContext>
652+
auto do_format(const join_view<It, Sentinel, Char>& value,
653+
FormatContext& ctx, It& it) const -> decltype(ctx.out()) {
638654
auto out = ctx.out();
639655
if (it != value.end) {
640656
out = value_formatter_.format(*it, ctx);
@@ -656,7 +672,7 @@ struct formatter<join_view<It, Sentinel, Char>, Char> {
656672
*/
657673
template <typename It, typename Sentinel>
658674
auto join(It begin, Sentinel end, string_view sep) -> join_view<It, Sentinel> {
659-
return {begin, end, sep};
675+
return {std::move(begin), end, sep};
660676
}
661677

662678
/**

test/ranges-test.cc

+53
Original file line numberDiff line numberDiff line change
@@ -652,3 +652,56 @@ struct lvalue_qualified_begin_end {
652652
TEST(ranges_test, lvalue_qualified_begin_end) {
653653
EXPECT_EQ(fmt::format("{}", lvalue_qualified_begin_end{}), "[1, 2, 3, 4, 5]");
654654
}
655+
656+
#if !defined(__cpp_lib_ranges) || __cpp_lib_ranges <= 202106L
657+
# define ENABLE_INPUT_RANGE_JOIN_TEST 0
658+
#elif FMT_CLANG_VERSION
659+
# if FMT_CLANG_VERSION > 1500
660+
# define ENABLE_INPUT_RANGE_JOIN_TEST 1
661+
# else
662+
# define ENABLE_INPUT_RANGE_JOIN_TEST 0
663+
# endif
664+
#else
665+
# define ENABLE_INPUT_RANGE_JOIN_TEST 1
666+
#endif
667+
668+
#if ENABLE_INPUT_RANGE_JOIN_TEST
669+
TEST(ranges_test, input_range_join) {
670+
std::istringstream iss("1 2 3 4 5");
671+
auto view = std::views::istream<std::string>(iss);
672+
auto joined_view = fmt::join(view.begin(), view.end(), ", ");
673+
EXPECT_EQ("1, 2, 3, 4, 5", fmt::format("{}", std::move(joined_view)));
674+
}
675+
676+
TEST(ranges_test, input_range_join_overload) {
677+
std::istringstream iss("1 2 3 4 5");
678+
EXPECT_EQ(
679+
"1.2.3.4.5",
680+
fmt::format("{}", fmt::join(std::views::istream<std::string>(iss), ".")));
681+
}
682+
#endif
683+
684+
TEST(ranges_test, std_istream_iterator_join) {
685+
std::istringstream iss("1 2 3 4 5");
686+
std::istream_iterator<int> first{iss};
687+
std::istream_iterator<int> last{};
688+
auto joined_view = fmt::join(first, last, ", ");
689+
EXPECT_EQ("1, 2, 3, 4, 5", fmt::format("{}", std::move(joined_view)));
690+
}
691+
692+
TEST(ranges_test, movable_only_istream_iter_join) {
693+
// Mirrors c++20 std::ranges::basic_istream_view::iterator
694+
struct UncopyableIstreamIter : std::istream_iterator<int> {
695+
explicit UncopyableIstreamIter(std::istringstream& iss)
696+
: std::istream_iterator<int>{iss} {}
697+
UncopyableIstreamIter(const UncopyableIstreamIter&) = delete;
698+
UncopyableIstreamIter(UncopyableIstreamIter&&) = default;
699+
};
700+
static_assert(!std::is_copy_constructible<UncopyableIstreamIter>::value, "");
701+
702+
std::istringstream iss("1 2 3 4 5");
703+
UncopyableIstreamIter first{iss};
704+
std::istream_iterator<int> last{};
705+
auto joined_view = fmt::join(std::move(first), last, ", ");
706+
EXPECT_EQ("1, 2, 3, 4, 5", fmt::format("{}", std::move(joined_view)));
707+
}

0 commit comments

Comments
 (0)