Passing and Returning std::array

When working with std::array, we pass it to functions like any other object. Since copying a std::array duplicates all its elements, we typically pass by const reference to avoid unnecessary copies.

A std::array's type includes both element type and length. When using std::array as a function parameter, we must explicitly specify both:

#include <array>
#include <iostream>

void showFirst(const std::array<int, 5>& scores) // must specify <int, 5>
{
    std::cout << scores[0] << '\n';
}

int main()
{
    std::array scores{ 85, 92, 78, 90, 88 }; // CTAD deduces std::array<int, 5>
    showFirst(scores);

    return 0;
}

CTAD (Class Template Argument Deduction) doesn't work with function parameters, so we cannot let the compiler deduce template arguments.

Using function templates for flexible types and lengths

To create functions that accept std::array with any element type or length, use a function template parameterizing both:

Since std::array is defined as:

template<typename T, std::size_t N> // N is a non-type template parameter
struct array;

We can create matching function templates:

#include <array>
#include <iostream>

template <typename T, std::size_t N>
void showFirst(const std::array<T, N>& items)
{
    static_assert(N != 0); // fail if zero-length std::array

    std::cout << items[0] << '\n';
}

int main()
{
    std::array scores{ 85, 92, 78, 90, 88 }; // CTAD infers std::array<int, 5>
    showFirst(scores);  // instantiates showFirst(const std::array<int, 5>&)

    std::array prices{ 19.99, 24.50, 15.75 }; // CTAD infers std::array<double, 3>
    showFirst(prices); // instantiates showFirst(const std::array<double, 3>&)

    std::array levels{ 1, 5, 10, 20, 35, 50 }; // CTAD infers std::array<int, 6>
    showFirst(levels); // instantiates showFirst(const std::array<int, 6>&)

    return 0;
}

The template parameter declaration template <typename T, std::size_t N> defines T as a type template parameter for element type and N as a non-type template parameter of type std::size_t for array length.

Warning
The non-type template parameter for `std::array` should be `std::size_t`, not `int`! This matches how `std::array` is defined. Using `int` causes a type mismatch since templates don't perform conversions.

When we call showFirst(scores) (where scores is std::array<int, 5>), the compiler instantiates void showFirst(const std::array<int, 5>& items).

We can also template only one parameter. Here we parameterize only length while fixing element type to int:

#include <array>
#include <iostream>

template <std::size_t N> // only length is templated
void showFirst(const std::array<int, N>& items) // element type is explicitly int
{
    static_assert(N != 0);

    std::cout << items[0] << '\n';
}

int main()
{
    std::array scores{ 85, 92, 78, 90, 88 }; // CTAD infers std::array<int, 5>
    showFirst(scores);  // ok: matches template

    std::array prices{ 19.99, 24.50, 15.75 }; // CTAD infers std::array<double, 3>
    showFirst(prices); // error: no matching function

    return 0;
}

Auto non-type template parameters (C++20)

Remembering the correct type for non-type template parameters is inconvenient.

C++20 allows auto in template parameter declarations to deduce the type:

#include <array>
#include <iostream>

template <typename T, auto N> // auto deduces type of N
void showFirst(const std::array<T, N>& items)
{
    static_assert(N != 0);

    std::cout << items[0] << '\n';
}

int main()
{
    std::array scores{ 85, 92, 78, 90, 88 };
    showFirst(scores);

    std::array prices{ 19.99, 24.50, 15.75 };
    showFirst(prices);

    std::array levels{ 1, 5, 10, 20, 35, 50 };
    showFirst(levels);

    return 0;
}

If your compiler supports C++20, this is the preferred approach.

Static assertions on array length

Consider this template function printing element at index 2:

#include <array>
#include <iostream>

template <typename T, std::size_t N>
void showThird(const std::array<T, N>& items)
{
    std::cout << items[2] << '\n';
}

int main()
{
    std::array scores{ 85, 92, 78, 90, 88 };
    showThird(scores);

    return 0;
}

This works correctly, but there's a lurking bug. The compiler allows arrays where index 2 is out of bounds:

#include <array>
#include <iostream>

template <typename T, std::size_t N>
void showThird(const std::array<T, N>& items)
{
    std::cout << items[2] << '\n'; // invalid if N < 3
}

int main()
{
    std::array pair{ 10, 20 }; // 2-element array (valid indexes 0 and 1)
    showThird(pair);

    return 0;
}

This causes undefined behavior. We want the compiler to catch this.

One solution: use std::get() (compile-time bounds checking) instead of operator[]:

#include <array>
#include <iostream>

template <typename T, std::size_t N>
void showThird(const std::array<T, N>& items)
{
    std::cout << std::get<2>(items) << '\n'; // compile-time bounds check
}

int main()
{
    std::array scores{ 85, 92, 78, 90, 88 };
    showThird(scores); // okay

    std::array pair{ 10, 20 };
    showThird(pair); // compile error

    return 0;
}

When the compiler instantiates showThird(const std::array<int, 2>&), std::get<2>(items) fails because index 2 is invalid for a 2-element array.

Alternative: use static_assert to validate preconditions:

#include <array>
#include <iostream>

template <typename T, std::size_t N>
void showThird(const std::array<T, N>& items)
{
    // precondition: array must have at least 3 elements
    static_assert(N > 2);

    std::cout << items[2] << '\n';
}

int main()
{
    std::array scores{ 85, 92, 78, 90, 88 };
    showThird(scores); // okay

    std::array pair{ 10, 20 };
    showThird(pair); // compile error

    return 0;
}

When instantiating showThird(const std::array<int, 2>&), static_assert(N > 2) fails because 2 > 2 is false.

Note: Prior to C++23, using std::size(data) in static assertions on array parameters doesn't compile due to a language defect. Use the template parameter N directly.

Returning a std::array

Passing std::array to functions is straightforward—pass by const reference. Returning is more complex. Unlike std::vector, std::array isn't move-capable, so returning by value copies the array. Elements are moved if move-capable, otherwise copied.

Two conventional approaches exist, chosen based on circumstances.

Returning by value

Returning by value is acceptable when:

  • The array isn't huge
  • Elements are cheap to copy (or move)
  • Code isn't performance-critical

A copy occurs, but if conditions are met, the impact is minor:

#include <array>
#include <iostream>
#include <limits>

template <typename T, std::size_t N>
std::array<T, N> readScores() // return by value
{
    std::array<T, N> result{};
    std::size_t index{ 0 };
    while (index < N)
    {
        std::cout << "Enter score #" << index << ": ";
        std::cin >> result[index];

        if (!std::cin) // handle bad input
        {
            std::cin.clear();
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            continue;
        }
        ++index;
    }

    return result;
}

int main()
{
    std::array<int, 5> scores{ readScores<int, 5>() };

    std::cout << "Third score: " << scores[2] << '\n';

    return 0;
}

Benefits:

  • Uses the most conventional return method
  • Clear that the function returns a value
  • Can define and initialize the array in one statement

Drawbacks:

  • Returns a copy of the array and all elements, which isn't cheap
  • Must explicitly provide template arguments since there are no parameters to deduce from

Returning via an out parameter

When return by value is too expensive, use an out-parameter. The caller passes the std::array by non-const reference, and the function modifies it:

#include <array>
#include <limits>
#include <iostream>

template <typename T, std::size_t N>
void readScores(std::array<T, N>& result) // pass by non-const reference
{
    std::size_t index{ 0 };
    while (index < N)
    {
        std::cout << "Enter score #" << index << ": ";
        std::cin >> result[index];

        if (!std::cin) // handle bad input
        {
            std::cin.clear();
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            continue;
        }
        ++index;
    }
}

int main()
{
    std::array<int, 5> scores{};
    readScores(scores);

    std::cout << "Third score: " << scores[2] << '\n';

    return 0;
}

Primary advantage: no copy occurs, making this efficient.

Drawbacks:

  • Non-conventional return method, not obvious that the function modifies the argument
  • Can only assign values, not initialize
  • Cannot produce temporary objects

Consider std::vector instead

std::vector is move-capable and can be returned by value without expensive copies. If returning a std::array by value, your std::array probably isn't constexpr, and you should consider using std::vector instead.

Summary

Passing std::array: Pass by const reference to avoid expensive copies. CTAD doesn't work with function parameters, so explicitly specify both element type and length, or use function templates to parameterize them.

Function templates for std::array: Create templates with type parameter T and non-type parameter std::size_t N (not int!) to accept arrays of any type or length. Can template both or just one.

Auto non-type template parameters (C++20): Use auto N instead of std::size_t N to let the compiler deduce the non-type parameter type.

Static assertions on length: Use static_assert with template parameter N to validate array length preconditions at compile-time, catching errors like accessing index 2 in a 2-element array.

Compile-time bounds checking: Use std::get<index>() for compile-time validation of constant indices, or static_assert(N > required_index) for minimum size requirements.

Returning by value: Acceptable when arrays are small, elements are cheap to copy/move, and code isn't performance-critical. Conventional but involves copying all elements.

Returning via out-parameter: More efficient (no copy) but non-conventional. Caller passes array by non-const reference, function modifies it. Cannot initialize arrays this way.

Consider std::vector instead: If returning non-constexpr std::array by value, std::vector may be better since it's move-capable.

The key insight is that std::array type information includes both element type and length, requiring explicit specification or template parameterization when used as function parameters.