Convert sequence of number into display string
1. Separate data representation from actual output, using standard algorithms where possible.

make_ranges below takes input sequence array and returns limits of all ranges found, in two different sequences from, to:

template&lt;typename F, typename T, typename A&gt;
void make_ranges(F&amp; from, T&amp; to, A array)
{
    auto diff = array;
    std::sort(array.begin(), array.end());
    std::adjacent_difference(array.begin(), array.end(), diff.begin());

    std::vector&lt;size_t&gt; idx(array.size());
    std::iota(idx.begin(), idx.end(), 0);
    auto sep = [&amp;diff] (size_t i) { return diff[i] &gt; 1; };
    for(auto p = idx.begin(); p != idx.end();)
    {
        from.push_back(array[*p]);
        p = std::find_if(++p, idx.end(), sep);
        to.push_back(array[*(p - 1)]);
    }
}


2. For most generic use, use a stream for output:

template&lt;typename S, typename I, typename J&gt;
void print_range(S&amp; stream, I i, J j)
{
    stream &lt;&lt; *i;
    if (*j != *i)
        stream &lt;&lt; "-" &lt;&lt; *j;
}

template&lt;typename S, typename F, typename T&gt;
void print_ranges(S&amp; stream, const F&amp; from, const T&amp; to)
{
    auto i = from.begin();
    auto j = to.begin();
    if (i != from.end())
        print_range(stream, i++, j++);
    while (i != from.end())
    {
        stream &lt;&lt; ",";
        print_range(stream, i++, j++);
    }
}


3. For convenient use, provide a streaming operator interface:

template&lt;typename S, typename A&gt;
S&amp; operator&lt;&lt;(S&amp; stream, const array_ranges&lt;A&gt;&amp; ranges)
{
    typename std::decay&lt;A&gt;::type from, to;
    make_ranges(from, to, std::get&lt;0&gt;(ranges));
    print_ranges(stream, from, to);
    return stream;
}


where array_ranges is a helper struct holding a sequence and indicating that its ranges should be streamed out instead of the sequence itself. Given a sequence, an array_ranges instance is made easily via helper function ranges:

template&lt;typename A&gt;
struct array_ranges : std::tuple&lt;A&gt; { using std::tuple&lt;A&gt;::tuple; };

template&lt;typename A&gt;
array_ranges&lt;A&gt; ranges(A&amp;&amp; a) { return array_ranges&lt;A&gt;{std::forward&lt;A&gt;(a)}; }


Example

Now the above can be used as

std::vector&lt;int&gt; arr{1,2,3,4,5,7,7,8,8,8,10,15,10,11,12,88,87,86};
std::cout &lt;&lt; arr &lt;&lt; std::endl;
std::cout &lt;&lt; ranges(arr) &lt;&lt; std::endl;


See live example.

If you want to print the ranges into a string, just use an std::ostringstream.

Discussion

With the above separation, you can do more advanced tasks like extracting ranges from arbitrary sequences, processing them (e.g. filtering or merging two sets of ranges), and finally printing them.

If you don't need any extra processing, this separation is more costly than direct printing since it stores intermediate results in arrays from, to. Using std::adjacent_difference introduces another intermediate result.

It is possible to save the extra cost of "intermediate results" while keeping the same design by more advanced techniques like expression templates, but this goes way beyond this simple task.

I would personally prefer a clean design to optimal performance for this task. This makes code more readable and maintainable. Also, with a bit more abstraction, you may find that parts of the code above can be reused elsewhere (especially print_ranges, but I don't want to go too far).

Another choice you could follow is to have an std::pair or (better) a dedicated data structure representing one range. A single sequence would then be enough instead of two sequences from, to, but processing such a sequence would be a bit more involved.

Of course, you are free to flat everything into a single call without intermediates if you prefer.