Exception-Free Builds
Tess supports Clang-family and GCC consumers compiled with
-fno-exceptions, and native MSVC consumers compiled with /EHs-c- and
_HAS_EXCEPTIONS=0. The installed tess::tess target remains neutral: the
consumer applies the recipe consistently to every translation unit in the
program. TESS_HAS_EXCEPTIONS and tess::has_exceptions report the compiler
mode and cannot be overridden by an application definition.
Exception-enabled builds remain the default. Their callback propagation, worker joining, and operation-specific rollback behavior are unchanged.
Failure Contract
Successful operations and explicit status results behave the same in both modes. Deterministic capacity checks reach non-throwing entry points two different ways, and the distinction matters to an exception-enabled caller.
A new _checked entry point was added beside the existing throwing one,
whose exception-enabled behavior is unchanged:
BlockScratch::reserve_bytes_checkedbesidereserve_bytes;WeightedPortalSegmentCache::reserve_segments_checkedandreserve_path_nodes_checkedbesidereserve_segmentsandreserve_path_nodes; andWeightedPortalSegmentCache::ClassView::store_checkedbesidestore.
The function itself stopped throwing, in both modes, and now reports through a status. There is no throwing wrapper for these:
collect_planned_dirty, which returned by throwingstd::length_errorand now returnsPlannedDirtyCollectStatus::CapacityExceeded; and- both partition-collecting
merge_planned_dirtyoverloads, which likewise now returnPlannedDirtyMergeStatus::CapacityExceeded.
An exception-enabled caller that upgraded across that change must check the
returned status rather than rely on catch. AutoExecTask does this
internally: it treats CapacityExceeded as a signal to run the
allocation-free fallback merge, which publishes every started callback's
dirty metadata, so the task's observable result is unchanged and the
condition does not escape run().
ReserveStatus, PortalSegmentStoreStatus,
PlannedDirtyCollectStatus, and PlannedDirtyMergeStatus report capacity
failure before the associated storage allocation, before any world mutation,
and before the source records are consumed — so a rejected operation can
still be retried or handled from its inputs. The throwing entry points in
the first list keep their exception-enabled behavior and abort through one
internal fail-fast path when a deterministic capacity error occurs in an
exception-free build.
Two details qualify that, and neither costs a caller its inputs:
- Caller-supplied scratch is not preserved. The
PlannedDirtyPartitionsoverload ofmerge_planned_dirtyclears the accumulator it is handed before collecting, so that accumulator is empty on aCapacityExceededreturn. The partitions themselves are untouched, which is what lets the allocation-free fallback merge publish them afterwards. - A portal-segment store already at its segment budget is bounded by how many entries survive compaction, which is only known after a full dependency-validity sweep. It rejects in constant time where it can, and otherwise captures the candidate entry's dependencies before the compaction returns the status. That capture appends to an unreserved vector, so a path crossing several chunks may reallocate more than once. Cache storage is still untouched, and general allocation failure is outside the contract either way.
General allocation failure, operating-system thread-creation failure, and an application operation that throws across an exception-free boundary are outside the supported recovery contract. This applies to callbacks, visitors, providers, kernels, predicates, allocators, and throwing value-type operations. Tess does not install a process-wide new or terminate handler.
Execution Policy
WorkerPoolPhaseExecutor and ScopedThreadPhaseExecutor select a
policy-specialized implementation from the compiler mode. Their exception-free
specializations omit callback exception state and cancellation coordination.
The implementation types are WorkerPoolPhaseExecutorImpl and
ScopedThreadPhaseExecutorImpl; applications should normally use the aliases.
NoThrowWorkerPoolPhaseExecutor and NoThrowScopedThreadPhaseExecutor expose
the same direct path to exception-enabled programs with an explicit no-throw
contract. Those aliases reject callbacks that are not typed noexcept when
compiler exceptions are enabled; exception-free builds continue to accept
ordinary callback types because the compiler mode supplies the contract.
An explicitly noexcept callback keeps that property through queued phase,
result-channel, schedule, and automatic-execution adapters. Existing erased
callback signatures remain source compatible; ScheduleNoThrowTaskFn is the
raw erased signature for callers that want to preserve the stronger contract.
Adapter setup completes all potentially growing internal storage before
entering the no-throw dispatch region, and allocation tests pin that invariant.
All translation units in a program must use the same exception mode. Mixed
mode programs and cross-mode ABI compatibility are unsupported.
tess::has_exceptions is an inline constexpr bool and the public executor
aliases are derived from it, so a mixed-mode program violates the
one-definition rule.
Detection of that mistake is uneven, and the build system carries the burden on every toolchain:
- GCC and Clang have no mechanism to detect it. A mixed-mode program links silently.
- MSVC gets a partial link-time check:
tess/core/config.hemits#pragma detect_mismatch("tess_exception_mode", ...)keyed on_CPPUNWIND, andtess/core/capacity.hdoes the same for the internal capacity-testing hook. Both only stamp translation units that include a Tess header, and neither observes the separate_HAS_EXCEPTIONSSTL switch, so a/EHscunit built with_HAS_EXCEPTIONS=0still passes.
Potentially Throwing Standard-Library Operations
The exception-free path uses the standard library normally. Deterministic size failures are prevalidated where Tess exposes a checked operation; resource failures remain outside the contract.
| Component | Operations on the path | Deterministic validation | Residual failure |
|---|---|---|---|
| Block scratch | make_unique_for_overwrite |
rounded byte count and overflow before growth | allocation failure |
| Portal segment cache | vector reserve, insertion, dependency capture, and compaction |
entry/path additions and compaction totals before live mutation | allocation or throwing element/provider operation |
| Queued dirty collection | vector reserve, insertion, and sort |
aggregate record count before source consumption | allocation during reserve |
| Scoped and pooled executors | vector growth, thread construction, mutexes, and condition variables |
operation ranges and status results | allocation, thread, or synchronization resource failure |
| Schedule and auto-exec | task/result vector growth and erased callback invocation | existing cadence, plan, and status validation | allocation or application callback failure |
| Stable maintenance and topology | container growth, maintenance callbacks, provider calls, moves, and swaps | existing bounds, handle identity, shape, and capacity checks before practical mutation points | allocation or throwing callback/provider/value operation |
| Storage and path algorithms | vector growth and caller-supplied policy operations | existing coordinate, shape, arithmetic, and explicit status checks | allocation or throwing caller operation |
The build does not disable unwind tables, frame pointers, debug information, or profiler support. Those choices are independent of C++ language exception handling.
Toolchains
CI compiles and runs the mode with Clang plus ASan/UBSan, GCC under warnings-as-errors, and native MSVC. It builds standalone headers, macro configurations, representative runtime behavior, an installed consumer, and a FetchContent consumer in each supported compiler family. Those consumers exercise the stable maintenance facade and immediate backend without RTTI as well as in the configured exception mode. Complete examples are additionally built and run with Clang or GCC.
AppleClang uses the Clang-family -fno-exceptions recipe, but CI does not
duplicate the exception-free contract suite on macOS. Linux Clang supplies
that language-mode coverage while ordinary macOS CI remains exception-enabled.
This intentionally contains CI time at the cost of not detecting an
AppleClang-specific exception-free regression in the repository matrix.
The exception-free contract targets are opt-in so ordinary developer and CI
build-all invocations do not compile both language modes. Configure them with
-DTESS_BUILD_NO_EXCEPTIONS_TESTING=ON; the normal build then includes the
exception-free runtime, consumer-contract, and macro-cell executables, so an
unfiltered ctest run has every registered executable. The standalone-header
verifier remains an explicit target because it registers no test. Focused
builds may select the relevant tess_no_exceptions_* targets before running
tests labeled config:noexceptions. CI keeps the focused MSVC contracts in a
job parallel to the existing full Windows build so this coverage does not
extend the critical path.
Native MSVC's /EHs-c- mode is not identical to -fno-exceptions: it does
not provide the same compile-time enforcement or safe recovery if an
exception is nevertheless thrown. Tess supports it as an exception-free by
construction configuration. The no-throw application-operation and resource
failure preconditions above are therefore especially important on MSVC.
Because /EHs-c- and _HAS_EXCEPTIONS=0 are separate switches, the
detect_mismatch check described above does not cover the STL half of the
recipe, so build-system consistency is still required to avoid an
unsupported mixed-mode program.
_HAS_EXCEPTIONS=0 is an MSVC STL implementation switch rather than a
supported public compiler mode. Microsoft STL maintainers have described it
as largely untested, undocumented, and unsupported.
Tess therefore treats native MSVC support as version-sensitive, pins it with
CI, and does not promise recovery from standard-library failures.