What about a syntax similar to this?
int main()
{
auto s = join("Hello ", ", World.", " The hex for ", 58, " is ", std::hex, 58);
std::cout << s << std::endl;
s = join(separator(" : "), "a", "b", std::hex, 200 , std::quoted("banana"));
std::cout << s << std::endl;
}
Which would produce the following output:
Hello , World. The hex for 58 is 3a
a : b : c8 : “banana"
sample implementation (io manipulators may be incomplete, some efficiency gains could be made by re-implementing ostringstream more cleverly):
#include <sstream>
#include <iostream>
#include <iomanip>
namespace detail {
template<class SepStr>
struct separator_object
{
template<class T>
std::ostream& operator ()(std::ostream& s, T&& t) const
{
return s << sep << t;
}
//
// other iomanp specialisations here
//
std::ostream& operator ()(std::ostream& s, std::ios_base&(*t)(std::ios_base&)) const
{
t(s);
return s;
}
SepStr const& sep;
};
struct no_separator_object
{
template<class T>
std::ostream& operator ()(std::ostream& s, T&& t) const
{
return s << t;
}
};
template<class Separator, class String, class...Rest>
auto join(Separator&& sep, String&& s, Rest&&...rest)
{
std::ostringstream ss;
ss << s;
using expand = int [];
void(expand{0,
((sep(ss, rest)), 0)...
});
return ss.str();
};
}
template<class Sep>
static constexpr auto separator(Sep const& sep)
{
using sep_type = std::remove_const_t<std::remove_reference_t<Sep>>;
return detail::separator_object<sep_type> { sep };
}
template<class SepObject, class String, class...Rest>
auto join(const detail::separator_object<SepObject>& sep, String&& s, Rest&&...rest)
{
return detail::join(sep,
std::forward<String>(s),
std::forward<Rest>(rest)...);
};
template<class String, class...Rest>
auto join(String&& s, Rest&&...rest)
{
return detail::join(detail::no_separator_object(),
std::forward<String>(s),
std::forward<Rest>(rest)...);
};
int main()
{
auto s = join("Hello ", ", World.", " The hex for ", 58, " is ", std::hex, 58);
std::cout << s << std::endl;
s = join(separator(" : "), "a", "b", std::hex, 200 , std::quoted("banana"));
std::cout << s << std::endl;
What about a syntax similar to this?
int main()
{
auto s = join("Hello ", ", World.", " The hex for ", 58, " is ",
std::hex, 58);
std::cout << s << std::endl;
s = join(separator(" : "), "a", "b", std::hex, 200 , std::quoted("banana"));
std::cout << s << std::endl;
}
Which would produce the following output:
Hello , World. The hex for 58 is 3a
a : b : c8 : “banana"
sample implementation (io manipulators may be incomplete, some efficiency
gains could be mad):
SepStr const& sep;
};
}
}
_______________________________________________
> Sorry to chime in so late in the discussion.
>
> What about a syntax similar to this?
>
> int main()
> {
> auto s = join("Hello ", ", World.", " The hex for ", 58, " is ",
> std::hex, 58);
> std::cout << s << std::endl;
>
> s = join(separator(" : "), "a", "b", std::hex, 200 ,
> std::quoted("banana"));
> std::cout << s << std::endl;
>
> }
>
> Which would produce the following output:
>
> Hello , World. The hex for 58 is 3a
> a : b : c8 : “banana"
>
The syntax is fine but it's missing an appending variant, like append(s,
"A", "B", 42);
This variant is important as it (also) allows you to reuse existing storage.
It has 2 improvements:
1. allows appending to an existing string by specifiying std::string&
join(onto(s), [optional separator("xxx"), parts...);
2. replaces use of std::ostringstream with a (very simple!) version that
does string appending.
#include <sstream>
#include <iostream>
#include <iomanip>
struct string_ref_buffer
: std::streambuf
{
using inherited = std::streambuf;
using char_type = inherited::char_type;
using char_traits = std::char_traits<char_type>;
int overflow(int c) override
{
if (c != char_traits::eof()) {
buffer_.push_back(c);
}
return char_traits::not_eof(c);
}
string_ref_buffer(std::string& buffer)
: buffer_(buffer)
{
}
const std::string& str() const & { return buffer_; }
std::string&& str()&& { return std::move(buffer_); }
std::string& buffer_;
std::size_t inpos_ = 0;
std::size_t outpos_ = 0;
};
namespace detail {
template<class SepStr>
struct separator_object
{
template<class T>
std::ostream& operator ()(std::ostream& s, T&& t) const
{
return s << sep << t;
}
//
// other iomanp specialisations here
//
std::ostream& operator ()(std::ostream& s,
std::ios_base&(*t)(std::ios_base&)) const
{
t(s);
return s;
}
SepStr const& sep;
};
struct no_separator_object
{
template<class T>
std::ostream& operator ()(std::ostream& s, T&& t) const
{
return s << t;
}
};
template<class Target, class Separator, class...Rest>
auto& join_onto(Target&& target, Separator&& sep, Rest&&...rest)
{
string_ref_buffer sbuf { target.str() };
std::ostream ss(std::addressof(sbuf));
using expand = int [];
void(expand{0,
((sep(ss, rest)), 0)...
});
return target;
};
template<class Separator, class String, class...Rest>
auto join(Separator&& sep, String&& s, Rest&&...rest)
{
std::string result {};
string_ref_buffer sbuf { result };
std::ostream ss(std::addressof(sbuf));
ss << s;
using expand = int [];
void(expand{0,
((sep(ss, rest)), 0)...
});
return result;
};
template<class String>
struct onto_type
{
String& str() { return target_.get(); }
std::reference_wrapper<String> target_;
};
}
template<class String>
auto onto(String& target)
{
return detail::onto_type<String> { target };
}
template<class Sep>
static constexpr auto separator(Sep const& sep)
{
using sep_type = std::remove_const_t<std::remove_reference_t<Sep>>;
return detail::separator_object<sep_type> { sep };
}
template<class SepObject, class String, class...Rest>
decltype(auto) join(detail::separator_object<SepObject> sep, String&&
s, Rest&&...rest)
{
return detail::join(sep,
std::forward<String>(s),
std::forward<Rest>(rest)...);
};
template<class String, class...Rest>
decltype(auto) join(String&& s, Rest&&...rest)
{
return detail::join(detail::no_separator_object(),
std::forward<String>(s),
std::forward<Rest>(rest)...);
};
template<class Target, class SepObject, class String, class...Rest>
decltype(auto) join(detail::onto_type<Target> target,
detail::separator_object<SepObject> sep, String&& s, Rest&&...rest)
{
return detail::join_onto(target,
sep,
std::forward<String>(s),
std::forward<Rest>(rest)...);
};
template<class Target, class String, class...Rest>
decltype(auto) join(detail::onto_type<Target> target, String&& s, Rest&&...rest)
{
return detail::join_onto(target,
detail::no_separator_object(),
std::forward<String>(s),
std::forward<Rest>(rest)...);
};
int main()
{
auto s= std::string("foo");
s = join("Hello ", ", World.", " The hex for ", 58, " is ", std::hex, 58);
std::cout << s << std::endl;
s = join(separator(" : "), "a", "b", std::hex, 200 , std::quoted("banana"));
std::cout << s << std::endl;
join(onto(s), separator(", "), "funky", "chicken");
join(onto(s), "=====");
std::cout << s << std::endl;
}
expected output:
Hello , World. The hex for 58 is 3a
a : b : c8 : "banana"
a : b : c8 : "banana", funky, chicken=====
On 18 January 2017 at 09:53, Richard Hodges <hodg...@gmail.com> wrote:
> That's pretty straightforward with another overload:
>
> auto& s = join(to(y), separator(", "), "A", "b", 42);
>
> where to(y) is something like
>
> template<String>
> struct to_existing_type<String> {
> String& get() { return s_; }
> String s_;
> };
>
> template<class String>
> auto to(String& s)
> {
> return to_existing_type<S>(s);
> }
>
> With a bit of template unwrapping, we could imagine something like this:
>
> join(to(x), 2, 3, to(y), "foo", "bar", create(), "baz", 42);
>
> which would return a tuple:
>
> std::tuple<std::string&, std::string&, std::string>
>
> in c++17 this would allow:
>
> auto&& [x, y, z] = join(to(x), 2, 3, to(y), "foo", "bar", create(), "baz",
> 42);
>
> But this maybe taking it a bit far... What do you think?
auto& s = join(to(y), separator(", "), "A", "b", 42);
where to(y) is something like
template<String>
struct to_existing_type<String> {
String& get() { return s_; }
String s_;
};
template<class String>
auto to(String& s)
{
return to_existing_type<S>(s);
}
With a bit of template unwrapping, we could imagine something like this:
join(to(x), 2, 3, to(y), "foo", "bar", create(), "baz", 42);
which would return a tuple:
std::tuple<std::string&, std::string&, std::string>
in c++17 this would allow:
auto&& [x, y, z] = join(to(x), 2, 3, to(y), "foo", "bar", create(), "baz",
42);
But this maybe taking it a bit far... What do you think?
On 18 January 2017 at 09:06, Olaf van der Spek <m...@vdspek.org> wrote:
> On 18 Jan 2017, at 10:59, Richard Hodges <hodg...@gmail.com> wrote:
>
> Ok. here's my second attempt.
I really like the expressive syntax of join(…). :) The implementation looks fine, too, but I am not an expert. In any case, I would love to use this.
Best regards,
Hans
> Would you prefer str(), or implicit conversion?
Please, no implicit conversion. I don't like .str() but it is better than implicit conversion. Implicit conversion is confusing, especially in the context of templates and auto.
I quote the Zen of Python: "Explicit is better than implicit".
> There is several usecases:
> […]
Ok, you made a point. Also, it follows the principle of least surprise if concat and friends all consistently return string factories.
> Am 24.01.2017 00:14, schrieb Gavin Lambert:
>> On 24/01/2017 07:26, Olaf van der Spek wrote:
>>>> 1. scope for formatting tags:
>>>> concat(format::hex<int>, 42, " is hex for ", concat(42)).str();
>>>> Here the inner concat will convert the 42 to its decimal representation,
>>>> while the outer one converts the first 42 to its hex representation.
>>> Wouldn't concat(hex(42), " is hex for", 42) make more sense?
>> +1. If you like persistent formatting states (and all the unexpected
>> fun they cause when you forget to cancel them), use iostreams instead.
>
> My proposal does not have anything similar to iostream manipulators. The conversion tags always extend to the whole concat parameter list, but not to enclosed calls to concat. There is no change in behavior that depends on the position of the conversion tag in the parameter list. Actually I think, we should be able to put all the conversion tags at the beginning of the parameter list.
+1 for concat(hex(42), " is hex for", 42)
If format::hex<int> is allowed to be in any position and still affect the whole string, that is not at all intuitive. What happens when you have two conflicting formatting tags in the argument list? Which one wins? Hard to judge for the user without looking into the reference manual. A design is intuitive if you don't need to look up stuff in the reference manual all the time. There is the principe of least surprise again.
If you use template magic to detect the conflicting formatting tags and raise a compile time error, you are still left the the natural exception of users that positions in "concat" matter. They matter for all the other arguments, so why shouldn't they matter for the formatting tags. It is breaking an implicit rule. Let's get rid of them to avoid the whole mess.
As other people pointed out, formatting tags should be left to streams, where they make sense. They don't make sense in concat, because the mental model for "concat" is "function call", not "stream". The most intuitive in this case to use explicit converters, unary functions like hex(42). Everyone who reads that can immediately understand what it does and that the call to hex(…) does not affect the other arguments in concat. Of course, hex(42) would return a string factory.
> join(hex<int>, separator(" "), my_nums);
>
> Here all the numbers are converted to their hex representation. With your approach this would look like:
>
> join(separator(" "),
> my_nums | transform([](int i) -> int {
> return hex(i);
> }));
>
> That is much more difficult to understand.
I don't see how that follows. You are free to write an overload for "join" which accepts an unary function as the first argument, which is then applied to all the values in the range. If hex<int> is an unary function, the first version makes even more sense.
The second version I reject regardless, because it uses an overloaded operator |. Operator overloads are ambiguous outside the realm of mathematics. We want to follow the principle of least surprise, and this is surprising syntax.
Hans
I didn't have time to look into boost::range, yet, so I didn't recognise this. Well that's a pity. :((
> I think, that composes less elegantly with boost::range or ranges::v3. Maybe we could lean towards their adaptor APIs like this:
>
> join(separator(" "), my_nums | hex<int>()));
If you use the design with the unary function in the beginning, it works for boost::range and classic iterators. You can still compose several unary functions by using a lambda.
> OK, here you made a point. In a specification I'd leave the behavior undefined. In an actual implementation I'd have the later ones overwrite the previous ones, because I expect that to be easy to implement. We could also try and prevent that with meta programming, but I think, that is not worth the effort. Leaving it unspecified in the specification, still leaves us the option to do that later.
If you can catch the error it at compile time, so that it costs nothing at runtime, it is certainly worth the effort.
> Still separator("...") is a bit out of the picture now. Up to mow I thought, that it is some kind of formatting information and therefore I handled it like other formatting tags. Actually I begin to like the idea of formatting functions, that return string factories. That fits very nicely with the rest of the API and makes concat(), join() and format() simpler.
>
> A completely different approach, inspired by Python:
>
> separator(" ").join(my_nums | hex<int>());
-1. It looks artificial in C++. In Python it is okay, because it creates a nice symmetry with .split(…). Here, there is no symmetry with .split. And any case, both should be methods of std::string then. For a user it will feel quite arbitrary that he/she has do use separator(" ").join(…) and instead of the simpler std::string(" ").join(…).
Am 24.01.2017 17:27, schrieb Hans Dembinski:
>> On 24 Jan 2017, at 12:54, Christof Donat <c...@okunah.de> wrote:
>> I think, that composes less elegantly with boost::range or ranges::v3.
>> Maybe we could lean towards their adaptor APIs like this:
>>
>> join(separator(" "), my_nums | hex<int>()));
>
> If you use the design with the unary function in the beginning, it
> works for boost::range and classic iterators. You can still compose
> several unary functions by using a lambda.
Sure, but a lambda is still more noise than a list of range adaptors (in
ranges::v3 they are called "views").
join(separator("\n"), my_bytes | hex<int>(fixed_size(2)) |
view::chunk(16) | join(separator(" "))).str();
Here I have added another, new idea. When join is only called without a
range, it returns a range view/adaptor, that expects to iterate over a
range of ranges and will return a range of string factories. The above
code would produce multiple lines with 16 hex representation of bytes
each.
>> OK, here you made a point. In a specification I'd leave the behavior
>> undefined. In an actual implementation I'd have the later ones
>> overwrite the previous ones, because I expect that to be easy to
>> implement. We could also try and prevent that with meta programming,
>> but I think, that is not worth the effort. Leaving it unspecified in
>> the specification, still leaves us the option to do that later.
>
> If you can catch the error it at compile time, so that it costs
> nothing at runtime, it is certainly worth the effort.
The issue is, that doing that is a lot of work. If we really want to do
stuff and not just talk about, what could possibly be done, I'd propose
to not implement it in the first release, but keep the behavior
undefined in the specification, so that it can be implemented later.
>> A completely different approach, inspired by Python:
>>
>> separator(" ").join(my_nums | hex<int>());
>
> -1. It looks artificial in C++. In Python it is okay, because it
> creates a nice symmetry with .split(…). Here, there is no symmetry
> with .split. And any case, both should be methods of std::string then.
> For a user it will feel quite arbitrary that he/she has do use
> separator(" ").join(…) and instead of the simpler std::string("
> ").join(…).
The latter is, in my opinion, even worse, because I really think,
std::string should not have many member functions. But yes, the syntax I
proposed here is not really intuitive in C++. Let's just forget about
it.
Christof
Why don't you add explicit conversion operators to the string factory
explicit operator std::string() { … }
explicit operator std::wstring() { … }
explicit type conversion was exactly added for this situation in C++11. The call is then
auto s = static_cast<std::string>(concat(…));
etc.
Otherwise I don't care if you add .str() or .to_string() members. I would prefer writing
auto s = concat(…).str();
over
auto s = static_cast<std::string>(concat(…));
But both should be supported.
Thank you Richard for this nice example, which illustrates the problem with implicit conversion. :)
It would be nice if there was a wiki for C++ with items like this ("don't do this and why") so that these explanations do not have to be written again and again.
>
> >> Note that I am still resisting the idea of .str() as a member function.
> If
> >> the joiner or concatenation object exports begin() and end(), it's
> >> un-necessary, because the object returned by create_string() (or
> similar)
> >> can use the iterators.
> >
> > I like that idea, though I still don't like the name to_string().
> >
> > How about this:
> >
> > auto s = generate<std::sting>(concat(...));
> > auto ws = generate<std::wsting>(concat(...));
>
> Why don't you add explicit conversion operators to the string factory
>
> explicit operator std::string() { … }
> explicit operator std::wstring() { … }
>
> explicit type conversion was exactly added for this situation in C++11.
> The call is then
>
> auto s = static_cast<std::string>(concat(…));
>
> etc.
>
> Otherwise I don't care if you add .str() or .to_string() members. I would
> prefer writing
>
> auto s = concat(…).str();
>
>
> over
>
> auto s = static_cast<std::string>(concat(…));
>
auto s = std::string(concat(…));
Or even
std::string s(concat(…));
?
Why bother with the static_cast?
--
Olaf
Am 26.01.2017 10:41, schrieb Hans Dembinski:
>>> Note that I am still resisting the idea of .str() as a member
>>> function. If
>>> the joiner or concatenation object exports begin() and end(), it's
>>> un-necessary, because the object returned by create_string() (or
>>> similar)
>>> can use the iterators.
>>
>> I like that idea, though I still don't like the name to_string().
>>
>> How about this:
>>
>> auto s = generate<std::sting>(concat(...));
>> auto ws = generate<std::wsting>(concat(...));
>
> Why don't you add explicit conversion operators to the string factory
>
> explicit operator std::string() { … }
> explicit operator std::wstring() { … }
>
> explicit type conversion was exactly added for this situation in
> C++11. The call is then
>
> auto s = static_cast<std::string>(concat(…));
That is the worst we had up to now. Even an implicit conversion is
better, which is not at all my favourite as well.
Christof
You don't know why the user added the overloads for foo, perhaps she suddenly had to adapt foo so that it also works with C code which uses a lot of const char*. As a designer, you have no control over other peoples' interfaces.
You argued about the principle of least surprise. Let's say the user started to use concat in her code with a function foo(std::string const & s). Then she decided to add to the overload foo(const char* s). All of a sudden her code does not compile anymore. This should not happen. As a user, she will be very surprised at that moment. That's why it is a bad idea to have implicit conversions.
Why do you think the standards committee added explicit operator <type>() to the language? They don't do these language changes for fun.
Also, if you won't take it from us, please go and take the wisdom from Herb Sutter
http://www.gotw.ca/gotw/019.htm
"It's almost always a good idea to avoid writing automatic conversions, either as conversion operators or as single-argument non-explicit constructors."
>> The call is then
>> auto s = static_cast<std::string>(concat(…));
>
> That is the worst we had up to now. Even an implicit conversion is better, which is not at all my favourite as well.
As Olaf pointed out, it is sufficient to do
auto s = std::string(concat(…));
you don't need the static_cast. Nevertheless, casts are the official way of doing type conversions, whether you like the syntax or not. I think Stroustrup intentionally made them ugly, because he wanted type conversions to be the exception in his statically typed language.
Hans
But only people, that have relied on implicit conversion, might have an
issue. Richard proposed to have member functions like .str(), free
functions like to_string() and implicit conversion. Use the explicit
member function, or free function, and you'll be fine. Whoever, for
whichever reason, decides to rely on implicit conversion, will probably
get easier to read code, but .
Generally, please calm down. I am not attacking you personally. I am
just discussing, weather implicit conversion might be a good idea as an
additional way to execute string factories. My current opinion is, that
it is not my preferred interface, but as an additional option, why not?
Only those, who use it, pay for it.
> Why do you think the standards committee added explicit operator
> <type>() to the language? They don't do these language changes for
> fun.
They also added the possibility to create implicit type conversions. If
implicit conversions really are so bad, as you put them, why did they do
that? For fun? Were they drunk?
And I really dislike explicit type conversions. Every time I had
considered to write an explicit type conversion, a member function, or a
free function with a speaking name was the better choice. So if we come
to the conclusion, that the implicit conversions harms anyone, but those
who use them, I'd vote for no conversion operators at all. Use the
explicit member functions then.
> Also, if you won't take it from us, please go and take the wisdom from
> Herb Sutter
>
> http://www.gotw.ca/gotw/019.htm
>
> "It's almost always a good idea to avoid writing automatic
> conversions, either as conversion operators or as single-argument
> non-explicit constructors."
It is almost always a good idea to listen to the wise people and then
think for yourself, if what they say really fits to your situation.
[Christof Donat, just now]
>>> The call is then
>>> auto s = static_cast<std::string>(concat(…));
>>
>> That is the worst we had up to now. Even an implicit conversion is
>> better, which is not at all my favourite as well.
>
> As Olaf pointed out, it is sufficient to do
>
> auto s = std::string(concat(…));
>
> you don't need the static_cast. Nevertheless, casts are the official
> way of doing type conversions, whether you like the syntax or not. I
> think Stroustrup intentionally made them ugly, because he wanted type
> conversions to be the exception in his statically typed language.
I totally agree with him. Explicit type conversions are ugly as hell,
and it is good, that way. Implicit type conversions might be dangerous,
so we should think twice, before we add them. But if in this case they
don't hurt anyone, then I think there is no good reason to not add them.
Christof