Simple string joiner in modern C++
Your code is fine, except for three small quirks


You don't need overloads if you provide default arguments
You don't need constant Ts
You don't need to check the length of your collection (except for begin == end).




template&lt;typename InputIt&gt;
std::string join(InputIt begin,
                 InputIt end,
                 const std::string &amp; separator =", ",  // see 1.
                 const std::string &amp; concluder ="")    // see 1.
{
    std::ostringstream ss;

    if(begin != end)
    {
        ss &lt;&lt; *begin++; // see 3.
    }    

    while(begin != end) // see 3.
    {
        ss &lt;&lt; separator;
        ss &lt;&lt; *begin++;
    }

    ss &lt;&lt; concluder;
    return ss.str();
}


You only need to check whether you have an empty range. If you don't, you start with the first one, and then stream the separator before the next element:

(0 elements case) &lt;concluder&gt;
(1 element  case) &lt;element&gt; &lt;concluder&gt;
(2 elements case) &lt;element&gt; &lt;separator&gt; &lt;element&gt; &lt;concluder&gt;
(3 elements case) &lt;element&gt; &lt;separator&gt; &lt;element&gt; &lt;separator&gt; &lt;element&gt; &lt;concluder&gt;


As you can see, except for the first element, you will always get &lt;separator&gt; &lt;element&gt; for any other element. This adds a little bit more logic to the empty case, but you don't need to traverse your range twice, and this method makes your algorithm eligible for input iterators.

Other than that, the only other thing I would change is to use an ostringstream instead of an stringstream. This prevents you from accidentally using &gt;&gt;. And global using std::*** isn't my cup of tee either. You can, however, move that into your main.

But here's a little exercise: you can just use an &lt;iterator&gt;:

template&lt;typename InputIt&gt;
std::string join(InputIt begin,
                 InputIt end,
                 const std::string &amp; separator =", ",  // see 1.
                 const std::string &amp; concluder ="")    // see 1.
{
    std::ostringstream ss;

    using value_type = typename std::iterator_traits&lt;InputIt&gt;::value_type;

    std::copy(begin, end, std::ostream_iterator&lt;value_type&gt;(ss, separator.c_str()));

    ss &lt;&lt; concluder;
    return ss.str();
}


However, as you've noted, it does not yield the same result as your variant. Here's the exercise, though: write an iterator that provides your behavior.