Skip to content
C++
Library
since C++17
Intermediate

Execution Policies

Tag types and global constants that control whether STL algorithms run sequentially, in parallel, or with SIMD vectorization — introduced in C++17.

Execution Policysince C++17

An execution policy is a tag type passed as the first argument to a parallelism-enabled STL algorithm to specify whether execution must be sequential, may use multiple threads, or may additionally interleave instructions across SIMD lanes.

Overview

Prior to C++17, every STL algorithm executed sequentially on the calling thread. C++17 introduced a parallel algorithm overload family: pass an execution policy as the first argument and the algorithm may exploit hardware parallelism. The policy is not a hint — it constrains the guarantees the implementation must uphold and the guarantees the caller must provide.

The <execution> header defines four policy types and one global constant of each type:

TypeGlobal constantThread modelSIMDSince
std::execution::sequenced_policystd::execution::seqsingle threadnoC++17
std::execution::parallel_policystd::execution::parmultiple threadsnoC++17
std::execution::parallel_unsequenced_policystd::execution::par_unseqmultiple threadsyesC++17
std::execution::unsequenced_policystd::execution::unseqsingle threadyesC++20

Sequenced (seq) — identical to the classic non-policy overload. Operations on element i happen-before operations on element i+1. Useful to enforce sequential semantics explicitly, or when benchmarking against the parallel variants.

Parallel (par) — the implementation may distribute loop iterations across an unspecified number of threads. Each individual element operation executes without interleaving with other element operations, so per-element work is effectively serialized within itself. The caller is responsible for ensuring no data races across elements and no deadlocks (e.g., no mutex that the algorithm's functor also tries to acquire on the same thread).

Parallel-unsequenced (par_unseq) — the implementation may additionally vectorize: a single thread may interleave instructions from multiple element operations within a single SIMD execution unit. This means element operations may overlap even on one thread. Consequently, the functor must not acquire mutexes, call new/delete, or perform any operation that is not safe to re-enter or interleave.

Unsequenced (unseq, C++20) — SIMD vectorization on a single thread. Like par_unseq but restricted to one thread, which makes it safe when the algorithm itself is already inside a parallel region managed by the caller.

C++17 specifies 77 standard algorithms that accept an execution policy overload. These include std::sort, std::transform, std::reduce, std::for_each, std::find, std::count_if, std::copy_if, and many others. std::accumulate is notably absent; its parallel equivalent is std::reduce.

Syntax

cpp
#include <algorithm>
#include <execution>
#include <numeric>
#include <vector>

std::sort(std::execution::par_unseq, v.begin(), v.end());   // C++17
std::reduce(std::execution::par, v.begin(), v.end(), 0);    // C++17
std::for_each(std::execution::unseq, v.begin(), v.end(), f); // C++20

The policy argument must be the first argument and must satisfy std::is_execution_policy_v<T>. Passing a policy to an algorithm that has no parallel overload is a compile error.

Examples

Parallel sort with timing

cpp
#include <algorithm>
#include <chrono>
#include <execution>
#include <iostream>
#include <random>
#include <vector>

int main() {
    std::vector<double> v(10'000'000);
    std::mt19937_64 rng{42};
    std::uniform_real_distribution<double> dist;
    std::generate(v.begin(), v.end(), [&] { return dist(rng); });

    auto data_par = v;
    auto data_seq = v;

    auto t0 = std::chrono::steady_clock::now();
    std::sort(std::execution::par_unseq, data_par.begin(), data_par.end()); // C++17
    auto t1 = std::chrono::steady_clock::now();
    std::sort(std::execution::seq, data_seq.begin(), data_seq.end());       // C++17
    auto t2 = std::chrono::steady_clock::now();

    std::cout << "par_unseq: "
              << std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count()
              << " ms\n";
    std::cout << "seq:       "
              << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
              << " ms\n";
}

Transform-reduce pipeline

std::transform_reduce (C++17) is the parallel-friendly substitute for the classic inner-product pattern:

cpp
#include <execution>
#include <numeric>
#include <vector>

double dot_product(const std::vector<double>& a, const std::vector<double>& b) {
    return std::transform_reduce(
        std::execution::par_unseq,          // C++17
        a.begin(), a.end(),
        b.begin(),
        0.0
    );
}

Correct accumulation into a shared result with par

When using par, concurrent writes to a shared variable are a data race. Use std::reduce (which requires commutativity and associativity) instead of manually accumulating:

cpp
// WRONG — data race: multiple threads write to `total` simultaneously
int total = 0;
std::for_each(std::execution::par, v.begin(), v.end(),
    [&](int x) { total += x; }); // undefined behaviour

// CORRECT — std::reduce handles parallelism internally
int total = std::reduce(std::execution::par, v.begin(), v.end(), 0); // C++17

Per-element mutation without sharing

par is safe when each functor invocation touches only its own element:

cpp
std::for_each(std::execution::par, v.begin(), v.end(),
    [](double& x) { x = std::sqrt(x); }); // safe: no shared state

Best Practices

Profile before committing to a policy. Parallel dispatch has non-trivial overhead (thread-pool wakeup, work stealing, SIMD lane alignment). For collections under roughly 10,000 elements, seq or no policy often wins. Measure with representative data.

Prefer par_unseq over par for pure transformations. When your functor has no locks and no heap allocation, par_unseq gives the implementation maximum freedom to vectorize, often yielding the best throughput.

Use std::reduce instead of std::accumulate when parallelizing sums. std::accumulate has no parallel overload. std::reduce requires the binary operation to be commutative and associative — floating-point addition technically violates strict associativity, but the difference in summation order is usually acceptable for practical workloads.

Reserve unseq (C++20) for code already running inside a parallel region. If you are inside an OpenMP parallel section or a thread-pool task and want vectorization without spawning additional threads, unseq is the right tool.

Check compiler and standard-library support. GCC links against Intel oneTBB or similar for par. MSVC ships its own parallel backend. Clang on macOS historically required explicit linking. Confirm <execution> is available before relying on it in portable code.

Common Pitfalls

Mutex acquisition inside a par_unseq functor causes undefined behaviour. The policy explicitly permits instruction interleaving within a single thread; trying to acquire a mutex from an interleaved context will deadlock or corrupt the mutex state. Restrict par_unseq functors to lock-free, allocation-free code.

Non-commutative binary operations with std::reduce produce indeterminate results. Unlike std::accumulate, std::reduce may reorder and group partial sums arbitrarily. Subtraction, string concatenation, and matrix multiplication are not safe binary operations for std::reduce without careful design.

Capturing mutable shared state in a par lambda without synchronization. A lambda that increments a shared counter without atomics or a mutex introduces a data race regardless of which policy is used — the policy controls scheduling, not synchronization.

Forgetting that par does not guarantee progress on the calling thread. The calling thread may or may not participate in the work. Waiting for a condition variable in the functor while holding a lock that the calling thread would otherwise release is a deadlock pattern specific to parallel execution.

Treating small workloads as trivially parallelizable. Thread-pool overhead can make par measurably slower than seq for small vectors. The crossover point depends on the per-element work; a square-root is cheap enough that you often need millions of elements before par pays off.

std::execution::unseq in Depth (C++20)

unseq was standardized separately from the C++17 trio, via WG21 paper P1001 (merged into C++20). Its design fills an important gap: before C++20 there was no standard way to request SIMD vectorization without also permitting the library to spawn additional threads.

Why the gap existed. C++17's par_unseq combines thread parallelism and SIMD permission into one policy. If your code is already running inside a thread-pool task or an OpenMP parallel region, asking for par_unseq risks over-subscribing the thread pool or launching threads that compete with the outer scheduler. unseq solves this by granting the compiler the same vectorization freedom as par_unseq — interleaving instructions from multiple element operations within a single SIMD execution unit — but with a strict single-thread guarantee.

What "unsequenced" means for the optimizer. When you pass unseq, the implementation is permitted to mark the loop body with a hint equivalent to #pragma loop(ivdep) (MSVC) or #pragma GCC ivdep, telling the auto-vectorizer that successive iterations carry no data dependencies. The runtime then executes the loop body with wide SIMD registers (SSE2, AVX2, AVX-512, NEON), processing multiple elements per instruction. The same prohibition on vectorization-unsafe operations that applies to par_unseq also applies here: no mutex acquisition, no operator new/delete, no calls to functions that internally synchronize.

Contrast with par_unseq. The two policies share identical restrictions on the functor but differ in their threading model:

PolicyThreadsSIMDSafe inside parallel region
par_unseqmultiple (library-managed)yesno — risks over-subscription
unseqone (calling thread only)yesyes

Practical use pattern. A typical scenario is vectorizing an inner loop inside an outer parallel region managed by OpenMP or TBB:

cpp
// Outer parallelism from TBB or OpenMP; inner loop uses unseq vectorization
tbb::parallel_for(tbb::blocked_range<size_t>(0, chunks),
    [&](const tbb::blocked_range<size_t>& r) {
        for (size_t i = r.begin(); i != r.end(); ++i) {
            std::transform(
                std::execution::unseq,  // C++20: SIMD, no extra threads
                data[i].begin(), data[i].end(),
                out[i].begin(),
                [](float x) { return x * x; }
            );
        }
    });

Detection macro. Use __cpp_lib_execution >= 201902L (updated value from C++20) to guard unseq usage in code that must compile against C++17 standard libraries.

Compiler and Runtime Backends

The C++ standard specifies the semantics of execution policies but says nothing about how implementations must parallelise the work. In practice the three major toolchains use entirely different mechanisms, with significant portability consequences.

GCC / libstdc++ (TBB backend)

GCC's libstdc++ delegates par and par_unseq to Intel's oneTBB (formerly Threading Building Blocks). TBB provides a work-stealing thread pool that scales automatically to the available hardware threads. The integration was introduced in GCC 9 and requires TBB 2018 or later.

Build requirement. You must link against the TBB library explicitly:

cpp
g++ -std=c++17 -O3 myfile.cpp -ltbb

Without -ltbb the linker will fail because libstdc++'s <execution> header pulls in references to TBB symbols. On Debian/Ubuntu: sudo apt install libtbb-dev. On RHEL/Fedora: sudo dnf install tbb-devel. The package manager usually also installs libtbbmalloc, which TBB recommends for its allocator scalability.

Feature-test guard. At compile time you can check __cpp_lib_parallel_algorithm (defined to 201603L in C++17 libstdc++ with TBB support) to confirm the feature is available before using it.

MSVC / MS-STL (Windows Thread Pool backend)

Microsoft's implementation was the earliest to ship (Visual Studio 2017 15.5, with the experimental tag removed in 15.7). It does not use TBB; instead it dispatches work to the Windows system thread pool via CreateThreadpoolWork/SubmitThreadpoolWork. This design composes correctly with COM apartments, SRWLOCK, NT events, and other OS synchronisation primitives — something TBB's user-mode scheduler does not.

Key limitation. As of Visual Studio 2022, MSVC implements par and par_unseq identically: both use threaded dispatch only. The SIMD "vectorization" freedom granted by par_unseq is not yet exploited to generate wider SIMD instructions beyond what the compiler's auto-vectoriser would produce anyway. The MSVC team has noted this explicitly: "you should not expect better performance for using par_unseq on our implementation." Choosing par_unseq over par on MSVC currently costs nothing but provides no extra benefit — except future-proofing if the implementation improves.

Several algorithms (including copy, fill, move, reverse, and rotate) are intentionally left serial because benchmarks showed them to be memory-bound: adding threads increases contention on the memory bus without reducing wall time. The MSVC team's documented policy is that an algorithm will only be parallelised if they can find a realistic benchmark where par wins for reasonable values of N.

Clang / libc++

libc++ has lagged on parallel algorithm support. As of Clang 18/19, the overloads are not available by default. There are two options:

  • -fexperimental-library enables a partial PSTL implementation, but it requires -ltbb and has known bugs.
  • Clang targeting Windows with the MSVC runtime (cl.exe + clang-cl) automatically uses the MS-STL backend and gets full support.

The practical guidance: if you need cross-platform code with parallel algorithms, guard with __cpp_lib_parallel_algorithm and provide a sequential fallback:

cpp
#ifdef __cpp_lib_parallel_algorithm
    std::sort(std::execution::par_unseq, v.begin(), v.end());
#else
    std::sort(v.begin(), v.end());
#endif

NVIDIA Thrust / NVHPC (GPU backend)

NVIDIA's HPC SDK and its Thrust library map par and par_unseq to CUDA kernels, distributing work across GPU thread blocks. AMD's ROCm stack provides a similar mapping via HIPSTDPAR (--hipstdpar compiler flag). These backends are the primary environments where par_unseq actually produces SIMD-equivalent wide execution — across GPU warps rather than CPU SIMD lanes.

Performance Threshold Guidance

The overhead of parallel dispatch is real and workload-dependent. There is no universal crossover point, but published benchmarks and implementation notes converge on the following rough guidelines.

The two key variables. Parallel speedup depends on two independent factors: the number of elements N, and the arithmetic intensity of the per-element operation (how many CPU cycles each element costs relative to memory latency). A cheap operation (e.g., a multiply) is memory-bound at almost any N; a transcendental function (e.g., std::sin, std::sqrt) is compute-bound and scales well.

Memory-bound operations (v * 2.0, element copy, integer increment). Thread-pool dispatch overhead often exceeds the time saved because every thread immediately stalls waiting for cache lines. Benchmarks on 6- to 12-core machines consistently show that par matches or beats seq only beyond N ≈ 1,000,000, and sometimes never beats it on bandwidth-saturated hardware. The reason is that adding threads does not add memory bandwidth — all cores share the same bus.

Compute-bound operations (std::sqrt(std::sin(v) * std::cos(v)), floating-point heavy work). Parallelism pays off much earlier. In practice, on 4- to 18-core machines, par starts beating seq around N ≈ 1,000 elements and delivers near-linear scaling up to N ≈ 1,000,000. The MSVC documentation gives a concrete rule of thumb: "algorithms that take more than O(n) time (like sort) and are called with N larger than 2,000 are good places to consider applying parallelism."

Sorting specifically. std::sort with par or par_unseq is an O(n log n) algorithm where the merge steps generate significant inter-thread communication. Even so, on a 10-core machine sorting 10 million double values, par_unseq typically runs 4–10x faster than seq depending on the memory subsystem. Sorting 1,000 doubles with par can be 10x slower than seq because thread-pool spinup alone takes longer than the sort.

Practical decision process. The MSVC team's design philosophy is the right model for everyone: benchmark your specific workload on your target hardware. The policies are not magic — they are contracts that unlock implementation freedom. Whether the implementation uses that freedom profitably is entirely workload- and platform-dependent.

cpp
// Minimal timing harness for policy comparison
#include <algorithm>
#include <chrono>
#include <execution>
#include <vector>

template<typename Policy>
double time_sort(Policy p, std::vector<double> v) {
    auto t0 = std::chrono::steady_clock::now();
    std::sort(p, v.begin(), v.end());
    auto t1 = std::chrono::steady_clock::now();
    return std::chrono::duration<double, std::milli>(t1 - t0).count();
}
// Compare: time_sort(std::execution::seq, data)
//      vs: time_sort(std::execution::par_unseq, data)
// for your actual N and element type before committing to par_unseq.

Relationship to C++26 std::execution (P2300)

C++17 execution policies and the C++26 sender/receiver model (P2300, merged as std::execution) address overlapping but distinct concerns. Understanding the difference prevents confusion when reading modern C++ proposals.

What C++17 policies express. An execution policy answers "how" the algorithm may execute: sequentially, in parallel threads, with SIMD, or both. It does not say where — which CPU core, which GPU, which thread pool — the work lands. The standard library's thread pool choice is entirely implementation-defined.

What P2300 schedulers express. A scheduler is an explicit handle to a specific execution context (a thread pool, a GPU stream, a strand). P2300 separates the where (scheduler) from the how (execution policy). A scheduler can be queried, composed, and passed through asynchronous pipelines as a first-class value.

How they interact (P2500 proposal). WG21 paper P2500 (targeted at C++26) proposes an execute_on(scheduler, policy) adaptor that binds a scheduler to an execution policy, yielding a policy_aware_scheduler. The resulting call site would look like:

cpp
// Hypothetical C++26 — not yet standardised
std::for_each(
    execute_on(my_thread_pool_scheduler, std::execution::par),
    v.begin(), v.end(), f
);

This retains the C++17 per-element semantics (the par policy still controls functor interleaving rules) while making the execution context explicit. The two models are thus complementary rather than competing.

Current state. P2300 was accepted into C++26. P2500 (the parallel-algorithms bridge) remains a work-in-progress proposal and is not yet part of any standard. For today's code, C++17 policies remain the only standard mechanism; P2300 senders are for asynchronous pipelines, not for annotating individual STL algorithm calls.

Avoid namespace collision. Both <execution> (C++17, execution policies) and the incoming C++26 std::execution (senders/receivers) live in the same namespace. The policy tag types (std::execution::par, etc.) are distinct from the sender/receiver concepts, but if you include both headers in the same translation unit, be explicit about which names you are using to avoid ambiguity in your team's code reviews.

Execution policies and exception safety

Unhandled exceptions inside parallel element operations terminate the program. When a functor throws during a par or par_unseq algorithm, the standard mandates that std::terminate be called — there is no way to propagate an exception from a worker thread back to the caller in a well-defined manner across an arbitrary number of simultaneous in-flight elements. The seq policy is the only one where exceptions propagate normally to the caller.

cpp
#include <algorithm>
#include <execution>
#include <stdexcept>
#include <vector>

// C++17
void safe_seq(std::vector<int>& v) {
    // Exception from element op propagates as normal throw/catch
    std::for_each(std::execution::seq, v.begin(), v.end(),
        [](int x) {
            if (x < 0) throw std::runtime_error("negative element");
        });
}

void unsafe_par(std::vector<int>& v) {
    // If the lambda throws, std::terminate() is called — no catch possible
    std::for_each(std::execution::par, v.begin(), v.end(),
        [](int x) {
            if (x < 0) throw std::runtime_error("this calls terminate!");
        });
}

Preferred pattern: replace throws with error flags. Store failure information in a std::atomic<bool> or a per-element error array, then check it after the algorithm completes. This preserves the performance of par while giving the caller a chance to handle the error.

cpp
#include <algorithm>
#include <atomic>
#include <execution>
#include <vector>

// C++17 — error-flag pattern for parallel exception handling
bool parallel_check(const std::vector<int>& v) {
    std::atomic<bool> had_error{false};
    std::for_each(std::execution::par, v.begin(), v.end(),
        [&](int x) {
            if (x < 0) had_error.store(true, std::memory_order_relaxed);
        });
    return had_error.load();
}

The same constraint applies to destructors: if a destructor called during element cleanup throws (itself undefined behaviour), the interaction with parallel execution policy makes the situation even less recoverable.

Profiling parallel algorithm overhead

Measuring the right thing matters more than choosing the right policy. A single wall-clock comparison between seq and par on one data size tells you very little; the crossover point shifts with N, element type, per-element cost, and NUMA topology.

cpp
#include <algorithm>
#include <chrono>
#include <cmath>
#include <execution>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>

// C++17 — sweep N to find the crossover point
int main() {
    for (std::size_t n : {1'000ULL, 10'000ULL, 100'000ULL,
                          1'000'000ULL, 10'000'000ULL}) {
        std::vector<double> v(n);
        std::iota(v.begin(), v.end(), 1.0);

        auto bench = [&](auto policy) -> double {
            auto w = v; // fresh copy each run
            auto t0 = std::chrono::steady_clock::now();
            std::transform(policy, w.begin(), w.end(), w.begin(),
                [](double x) { return std::sqrt(std::log(x)); }); // compute-bound
            auto t1 = std::chrono::steady_clock::now();
            return std::chrono::duration<double, std::milli>(t1 - t0).count();
        };

        double t_seq = bench(std::execution::seq);        // C++17
        double t_par = bench(std::execution::par_unseq);  // C++17
        std::cout << "N=" << std::setw(10) << n
                  << "  seq=" << std::setw(8) << t_seq << " ms"
                  << "  par_unseq=" << std::setw(8) << t_par << " ms"
                  << "  ratio=" << t_seq / t_par << "x\n";
    }
}

Watch for thread-pool warmup skew. The first call to a par algorithm on a cold process pays the thread-pool initialisation cost (sometimes 1–10 ms). Always warm up the pool with a small dummy sort before your real timing loop, or discard the first measurement.

cpp
// C++17 — warm up the thread pool before measuring
{
    std::vector<int> dummy(1000);
    std::sort(std::execution::par, dummy.begin(), dummy.end());
}
// Now start your real timing loop

Custom execution policies (C++26 preview)

The C++ standard deliberately left the door open for user-defined execution policies. The requirements for a valid execution policy type are minimal: it must satisfy std::is_execution_policy_v<T>, which is specialisable. Library vendors (TBB, HPX, Thrust) have long exploited this to expose non-standard policies as extensions.

cpp
// Hypothetical vendor extension — not standard C++17/20
// HPX exposes its own policy type as a drop-in replacement:
#ifdef HAS_HPX
#include <hpx/execution.hpp>
// hpx::execution::par can be passed to std::sort, std::transform, etc.
// because hpx::is_execution_policy<hpx::execution::par_type> is specialised.
std::sort(hpx::execution::par, v.begin(), v.end());
#endif

P2500 and P3300 together (targeting C++26) extend this to first-class scheduler binding. The goal is to let callers supply an explicit scheduler alongside a policy so that "where" (execution context) and "how" (element interleaving rules) are separately controllable.

cpp
// Hypothetical C++26 — based on P2500 draft
// Combines a thread-pool scheduler with the par interleaving contract.
// Not yet in any standard; syntax may change before ratification.
namespace stdex = std::execution; // C++26 sender/receiver namespace

auto result = std::for_each(
    stdex::execute_on(my_pool.scheduler(), stdex::par), // C++26 draft
    v.begin(), v.end(),
    [](int& x) { x *= 2; }
);

For today's code, prefer the four standard policies. Custom policies are only warranted when integrating with an existing runtime (HPX, SYCL, Thrust) that already provides its own policy type. Rolling your own policy from scratch gives you no benefit over using par unless your execution context has fundamentally different scheduling semantics.

seq policy for debugging

std::execution::seq is the most valuable debugging tool in the parallel algorithms toolkit. Because seq executes on the calling thread with fully sequenced element operations, it restores the deterministic ordering that data races and nondeterministic scheduling destroy. Replacing par with seq is often the fastest way to determine whether a bug is a parallelism issue or a logic issue.

cpp
#include <algorithm>
#include <execution>
#include <vector>

// C++17 — toggle policy via a compile-time constant for easy debugging
#ifdef NDEBUG
    constexpr auto kPolicy = std::execution::par_unseq;
#else
    constexpr auto kPolicy = std::execution::seq; // deterministic in debug builds
#endif

void process(std::vector<double>& v) {
    std::transform(kPolicy, v.begin(), v.end(), v.begin(),
        [](double x) { return x * x; });
}

seq also permits use of thread-unsafe tools inside the functor. If you need to log per-element output for debugging, call a non-thread-safe printf/std::cout, or use an ASAN/UBSan-instrumented check that is not signal-handler-safe, switching to seq makes all of that valid without modifying the functor itself.

cpp
// C++17 — safe per-element logging under seq
std::for_each(std::execution::seq, v.begin(), v.end(),
    [](double x) {
        // std::cout is NOT safe under par/par_unseq without synchronisation;
        // under seq it is fine because only the calling thread executes here.
        std::cout << "processing: " << x << '\n';
    });

Sanitizer note. ThreadSanitizer (TSan) and AddressSanitizer (ASan) interact poorly with some TBB internals. If TSan reports false positives inside par algorithm calls, switching the debug build to seq eliminates the false positives and makes genuine races in the functor logic visible.

Combining execution policies with ranges

Standard ranges algorithms (std::ranges::sort, std::ranges::transform, etc.) do not accept execution policies as of C++23. The parallel overloads and the ranges overloads are separate families defined in different parts of the standard. Attempting to pass std::execution::par to a std::ranges:: algorithm is a compile error.

cpp
#include <algorithm>
#include <execution>
#include <ranges>
#include <vector>

std::vector<int> v = {3, 1, 4, 1, 5, 9};

// C++20 — compiles fine: ranges sort, no policy
std::ranges::sort(v);

// C++17 — compiles fine: classic sort with policy
std::sort(std::execution::par, v.begin(), v.end());

// ERROR — does not compile: ranges overload has no policy parameter
// std::ranges::sort(std::execution::par, v);  // ill-formed

The workaround today is to bridge ranges and parallel algorithms via explicit iterator pairs. Extract the begin/end iterators from a range and pass them to the non-ranges parallel overload. This loses range-projection convenience but recovers parallel execution.

cpp
#include <algorithm>
#include <execution>
#include <ranges>
#include <vector>

// C++20 + C++17 — parallel sort over a subrange
auto subrange = std::ranges::subrange(v.begin() + 1, v.end() - 1);
std::sort(std::execution::par_unseq,          // C++17 parallel overload
          subrange.begin(), subrange.end());   // iterators from a range object

P2500 and the C++26 roadmap aim to unify these two families. The proposal introduces std::ranges::for_each and similar overloads that accept a scheduler-policy pair, finally giving ranges algorithms first-class parallel execution support. Until those are standardised, the iterator-bridge pattern above is the idiomatic solution.

See Also

  • std::reduce — parallel-safe accumulation requiring commutativity and associativity (C++17)
  • std::transform_reduce — fused parallel map-reduce pipeline (C++17)
  • std::for_each parallel overload — parallel application of a side-effect functor (C++17)
  • std::is_execution_policy — trait to detect execution policy types (C++17)
  • reference/library/concurrency/execution — lower-level executor and scheduling primitives