The code below explores different implementations of range(n), a function that returns the vector<int> {0, 1, 2, ..., n - 1}.

// range.cpp

#include <iostream>
#include <vector>
#include <memory>

using namespace std;

template<class T>
ostream& operator<<(ostream& os, const vector<T>& v) {
    int n = v.size();
    if (n == 0) {
        os << "{}";
    } else if (n == 1) {
        os << "{" << v[0] << "}";
    } else {
        os << "{" << v[0];
        for(int i = 1; i < v.size(); ++i) {
            os << ", " << v[i];
        }
        os << "}";
    }
    return os;
}

// By default, C++ returns a copy of the value that it returns in a function.
// So, for functions like range (below), this can be expensive. However, since
// C++11, there's a neat trick called "move semantics" that can, in some
// cases, le the compiler optimize-away the need to make a copy of the
// returned value. Code like range is now actually quite efficient, since it
// probably doesn't make a copy of the result vector (but instead returns a
// copy of the underlying pointer to the array in the vector).
vector<int> range(int n) {
        if (n <= 0) n = 0;
        vector<int> result(n);
        for(int i = 0; i < n; ++i) {
                result[i] = i;
        }
        return result;
}

// This version of range creates a vector on the free store using new, and
// then returns a pointer to it. The pointer is copied when returned, but the
// vector it points to is not copied. Thus it doesn't pointlessly copy the
// result. However, it introduces a potentially more serious and subtle bug:
// it is up to the code that calls range_raw_ptr to remember to call delete to
// avoid a memory leak. In practice, this is easier said than done. If you
// forget to call delete, you get a memory leak, i.e. the vector is never
// deleted and the memory it takes up cannot be re-used. If you call delete
// too soon, you may get pointer de-referencing errors. Also, you must ensure
// that you call delete exactly *once*; calling delete two or more times on
// the same pointer is usually a serious error that corrupts memory.
vector<int>* range_raw_ptr(int n) {
        if (n <= 0) n = 0;

        // new vector<int>(n) returns a pointer to a newly created vector<int> on
        // the free store
        vector<int>* result = new vector<int>(n);
        for(int i = 0; i < n; ++i) {
                (*result)[i] = i;
        }
        return result;
}

// range_unique_ptr follows the same idea as range_raw_ptr, but it uses a
// unique_ptr instead of a raw pointer. The value of this is that a unique_ptr
// will automatically (and correctly!) delete the memory it refers to so there
// are no memories leaks or other problems with pointers. In practice, this is
// often very useful, and a good reason to avoid raw pointers. C++ has other
// similar pointer classes for more complex situations, but for us unique_ptr
// should be enough.
//
// Please note that this is probably not the best way to actually write range.
// The first version that returns a plain vector<int> is probably the best
// in C++11 and later.
unique_ptr<vector<int>> range_unique_ptr(int n) {
        if (n <= 0) n = 0;
        unique_ptr<vector<int>> result(new vector<int>(n));
        for(int i = 0; i < n; ++i) {
                (*result)[i] = i;
        }
        return result;
}

void test_range() {
        vector<int> a = range(10);
        cout << a << "\n";

        vector<int>* b = range_raw_ptr(10);
        cout << *b << "\n";
        delete b;

        unique_ptr<vector<int>> c = range_unique_ptr(10);
        cout << *c << "\n";
    // no need to delete c --- unique_ptr automatically deletes the object it
    // points to when it goes out of scope
}

int main() {
        test_range();
}