std::join()
: An algorithm for joining a range of elementsISO/IEC JTC1 SC22 WG21 N3594 - 2013-03-13
Greg Miller, jgm@google.com 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:
std::split
)std::string_view
)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
.
sep
— a separator string to be put between each
formatted element in the output string.
std::string
reference. If no
Formatter is explicitly given by the caller, a default will be
used.
std::string
containing the elements in the Range
each separated by the given separator.
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");
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:
std::string
std::string_view
const char*
char*
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<<
.
std::string&
.
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");
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.
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-");
std::join()
be able to return types other than
std::string
?
std::string
and return it by value, or should it append the
formatted value to a provided std::string&
as is
described in this paper? This paper describes the appending solution for
performance reasons.
std::string
or is there a better "appendable" interface that
should be used?