std::join(): An algorithm for joining a range of elements

ISO/IEC JTC1 SC22 WG21 N3594 - 2013-03-13

Greg Miller, [email protected]

Introduction

Creating a string by joining the elements in a collection with a separator between each element is a common task in many applications. Often the elements themselves are strings, so joining them is fairly straight forward. However, it is not uncommon to join collections of types other than string. For example, one might want to log a collection of int identifiers with each value separated by a comma and a space. In this case there needs to be some way to format the collection's element type as a string.

The std::join() API described in this paper describes a function that is easy to use in all the common cases, such as joining strings and string-like objects as well as primitives like int, float, etc. This API is also extensible through a Formatter function object and is able to join arbitrary types.

Joining strings is the inverse of splitting strings and they are often thought of together. This proposal describes a joining function to go along with the std::split() function described in N3593 (std::split).

This proposal depends on or refers to the following proposals:

API Synopsis

std::join()

The function called to join a Range of elements with a separator into a single output string.

    namespace std {

      // Range and Formatter
      template <typename Range, typename Formatter>
      std::string join(const Range& range, std::string_view sep, Formatter f);

      // Range (a default formatter is used)
      template <typename Range>
      std::string join(const Range& range, std::string_view sep);

    }
    

There are two versions of std::join() that differ in that one of them takes an explicit Formatter function object and the other one uses a default Formatter. Both take a Range of T where T can be any object that is compatible with the Formatter (whether the default Formatter or the one explicitly given). Both std::join() functions return the result as a std::string.

Requires:
Range — a Range of elements that can be formatted using the specified (or default) Formatter. This argument should fully model the Range concept.
sep — a separator string to be put between each formatted element in the output string.
Formatter — a function object (or a lambda) that can format the Range's element types and will append the formatted elements to the provided std::string reference. If no Formatter is explicitly given by the caller, a default will be used.
Returns:
A std::string containing the elements in the Range each separated by the given separator.

Formatter template parameter

A Formatter is a function object that is responsible for formatting a type T and appending it the the given std::string&. A Formatter must have a function call operator with the following signature.

    void operator()(std::string& output, T n);
    

Where the Range's element type must be convertible to T.

The following is an example formatter that can format any numeric type that is compatible with std::to_string.

    
    struct number_formatter {
      template <typename Number>
      void operator()(std::string& output, Number n) const {
      std::string s = std::to_string(n);
      output.append(s.data(), s.size());
      }
    };

    // Uses the number_formatter to join a vector of ints.
    vector<int> v1{1, 2, 3};
    std::string s1 = std::join(v1, "-", number_formatter());
    assert(s1 == "1-2-3");

    // Uses the number_formatter to join a vector of doubles.
    vector<double> v2{1.1, 2.2, 3.3};
    std::string s2 = std::join(v2, "-", number_formatter());
    assert(s2 == "1.1-2.2-3.3");
    
    

Default Formatter

When the two-argument form of std::join() is called no Formatter is explicitly given, so a default Formatter will be used. The default formatter will be able to format the following types:

The above types should be formatted in the expected way; the same as would be done if the object was output with std::to_string or with operator<<.

Requires:
Ability to format string-like objects and all primitive types.
Returns:
Nothing.
Side-effects:
The argument is formatted and appended to the given std::string&.

API Usage

  1. This example shows joining various types of containers of various element types. All of these examples will use the default formatter to convert the non-string-like elements into strings.

        
        std::vector<string> vs{"foo", "bar", "baz"};
        std::string svs = std::join(vs, "-");
        assert(svs == "foo-bar-baz");
    
        std::vector<const char*> vc{"foo", "bar", "baz"};
        std::string svc = std::join(vc, "-");
        assert(svc == "foo-bar-baz");
    
        std::vector<int> vi{1, 2, 3};
        std::string svi = std::join(vi, "-");
        assert(svi == "1-2-3");
    
        double da[] = {1.1, 2.2, 3.3};
        std::string sda = std::join(da, "-");
        assert(sda == "1.1-2.2-3.3");
        
        
  2. This example shows the creation and use of a Formatter that knows how to format std::pair<const std::string, int> objects, with the first and second memebers of the pair separated by their own separator. This formatter could be used when joining a std::map.

        
        class pair_formatter {
          std::string sep_;
         public:
          pair_formatter(std::string_view sep)
          : sep_(static_cast<std::string>(sep)))
          {}
          void operator()(std::string& out, const std::pair<const std::string, int>& p) const {
            out.append(p.first);
            out.append(sep_);
            out.append(std::to_string(p.second));
        };
    
        // Example use of the pair_formatter
        std::map<std::string, int> m = {
          std::make_pair("a", 1),
          std::make_pair("b", 2)
        };
        std::string s = std::join(m, ",", pair_formatter("="));
        assert(s == "a=1,b=2");  // Actual order may vary.
        
        

    The pair_formatter example above could be made a template to work with pairs of arbitrary types T and U.

  3. The following examples show a handful of edge cases to show how they will be handled.

        
        // Joining an empty range
        std::vector<std::string> vempty;
        std::string sempty = std::join(vempty, "-");
        assert(sempty == "");
    
        // Joining a range of one element
        std::vector<std::string> vone{"foo"};
        std::string sone = std::join(vone, "-");
        assert(sone == "foo");
    
        // Joining a range of a single element that is the empty string
        std::vector<std::string> vone_empty{""};
        std::string sone_empty = std::join(vone_empty, "-");
        assert(sone_empty == "");
    
        // Joining a range of two elements with one being the empty string
        std::vector<std::string> vtwo{"foo", ""};
        std::string stwo = std::join(vtwo, "-");
        assert(stwo == "foo-");
        
        

Open Questions