Search code examples
c++c++23fmt

Implement join method with std::format


I am trying to implement a similar function as fmt::join with the difference that it needs to be used in conjunction with std::format

I have sofar the following example (link):

#include <ranges>
#include <vector>
#include <format>
#include <functional>

template<std::ranges::range RANGE>
struct JoinView {
    RANGE m_range;
    std::string_view m_sep;
    using Iter = std::ranges::iterator_t<RANGE>;
    using Sentinel = std::ranges::sentinel_t<RANGE>;
    JoinView(RANGE&& range, std::string_view sep) : m_range(std::move(range)), m_sep(sep) {};

    auto begin() const {
        return std::begin(m_range);
    }

    auto end() const {
        return std::end(m_range);
    }
};
template<typename INPUT_IT, typename OUTPUT_IT>
constexpr auto copy_str(INPUT_IT begin, INPUT_IT end, OUTPUT_IT out) -> OUTPUT_IT {
    while (begin != end) *out++ = static_cast<char>(*begin++);
    return out;
}
template<std::ranges::range RANGE>
auto join_detail(RANGE&& range, std::string_view sep) -> JoinView<RANGE>
{
    return { std::forward<RANGE>(range), sep };
}

template<std::ranges::range RANGE, typename Projection = std::identity>
inline auto join(RANGE&& range, std::string_view sep, Projection&& proj = std::identity{}) {
    auto joined = range
                  | std::views::transform([&proj](const auto& element) {
                      return std::invoke(proj, element);
                  })
                  | std::views::join_with(sep);
    
    return join_detail(std::move(joined), sep);

};

template <std::ranges::range RANGE>
struct std::formatter<JoinView<RANGE>, char> {
private:
    using ValueType = std::iter_value_t<typename JoinView<RANGE>::Iter>;
    formatter<std::remove_cvref_t<ValueType>, char> m_value_formatter;

public:
    template <typename PARSE_CONTEXT>
    constexpr auto parse(PARSE_CONTEXT& ctx) -> const char* {
        return m_value_formatter.parse(ctx);
    }

    template <typename FORMAT_CONTEXT>
    auto format(const JoinView<RANGE>& value, FORMAT_CONTEXT& ctx) const -> decltype(ctx.out()) {

        auto it = value.begin();
        auto out = ctx.out();
        if (it != value.end()) {
            out = m_value_formatter.format(*it, ctx);
            ++it;
            while (it != value.end()) {
                out = copy_str(value.m_sep.begin(), value.m_sep.end(), out);
                ctx.advance_to(out);
                out = m_value_formatter.format(*it, ctx);
                ++it;
            }
        }
        return out;
    }
};

int main() {

    auto dd = std::format("{}", join( std::array<int,3>{2,3,4}, ",",
        [](const auto& e){return std::format("{}", e);}));

}

I am stuck at the following compiler error:

<source>:15:16: error: no matching function for call to 'begin'
   15 |         return std::begin(m_range);

which I am struggling to understand since m_range seems to satisfy the std::ranges::range concept. Any idea what is wrong with the code and how it can be corrected


Solution

  • Sure, RANGE satisfies std::ranges::range, but const RANGE& (which is what you have in a member function) doesn't.

    The quickest-and-dirtiest fix (making RANGE m_range; mutable) makes this compile but gives incorrect output: https://godbolt.org/z/sxPbMGzsq (and probably isn't advisable because of const-correctness)

    Also you should call std::ranges::begin/end instead of std::begin/end.

    You eventually want to work towards non-const begin()/end() and take JoinView<RANGE> value by-value (since std::format_args copies the JoinView and calls std::formatter with a const JoinView).