September 8, 2017·6 min read·engineeringcppstandardslifetimestypes

C++17 wins unanimous approval with clearer value vocabulary

The C++17 DIS ballot receives unanimous approval and no technical comments. optional illustrates how the revision expresses absence and contained-object lifetime directly in an interface.

A function returning an integer can reserve a special value to mean absence, but then every caller must know which value carries that meaning. C++17 adds optional so that an interface can distinguish a present object from no object without taking a value away from the object's own domain. That is one practical change in a revision whose final major approval ballot has now completed unanimously.

Herb Sutter's approval report, published September 6 and revised September 8, records 100 percent approval of the C++17 Draft International Standard. The ballot produced 23 editorial comments and no technical comments. Unanimous approval permits the project to skip the FDIS ballot and proceed toward publication after editorial processing.

This is a different milestone from March's completion of the technical work. The report says the editor and helpers still need to address editorial comments and deliver the final document to ISO. Approval does not mean the final publication is already on sale, nor does it establish that every compiler and library has implemented every facility.

I value the parts of C++ that make lifetime and ownership visible in a type. optional is useful in that respect because it owns the storage for its contained object and records whether that object exists. The declaration can communicate absence without forcing a caller to infer ownership from a pointer convention.

Absence belongs to the result type

Thomas Köppe's C++17 change catalogue describes optional<T> as representing either a T value or no value. The nullopt indicator gives the latter state an explicit spelling. This distinguishes absence from an ordinary value such as zero, which may be a valid result in its own right.

This example uses the C++17 interface to retrieve an entry from a sequence. An out-of-range position means no result, while a stored zero remains a present result.

#include <cstddef>
#include <optional>
#include <vector>

std::optional<int> entry_at(const std::vector<int>& values,
                            std::size_t position)
{
    if (position >= values.size()) {
        return std::nullopt;
    }
    return values[position];
}

The function returns a value object. It does not return a reference into the vector or ask the caller to release dynamically allocated result storage. Those are separate possible interface policies, but this declaration makes its selected policy explicit through optional<int>.

That can simplify the agreement between caller and implementation. Every int remains available as a real value. The implementation has a distinct way to report that the requested element is absent, and the caller can check that state before using the contained integer.

It does not mean that absence is always the correct error model. This example deliberately defines an out-of-range position as no result. Another operation may need to explain why it failed, which requires additional information. optional expresses one particular distinction, not a general replacement for all exception or error-reporting designs.

The contained object's lifetime is managed separately

The March N4659 working draft explains the optional-object model. An optional object contains storage for another object and manages that contained object's lifetime. The inner object can be initialized after the wrapper and destroyed before the wrapper itself is destroyed.

This is more precise than describing an optional as an object accompanied by a Boolean. When it is empty, there is no initialized contained value whose operations the program may use. The wrapper tracks the distinction and performs the appropriate construction or destruction when its state changes.

The draft requires the contained value to occupy appropriately aligned storage within the optional object. Implementations may not allocate separate dynamic storage for that contained object. That is a requirement on the wrapper's representation strategy. It does not promise that T itself performs no dynamic allocation during its own construction.

The distinction is useful for a type such as an owning string. Placing that object inside an optional does not add a separate allocation merely to store the string object, but the string's own representation can still have its ordinary allocation behavior. Claims about allocation need to identify which object and operation they describe.

reset() ends the contained object's lifetime if one exists and leaves the optional empty. It has no effect on an already empty optional. This gives code a direct operation for releasing the contained value while retaining the wrapper object for possible later use.

Access operations express different caller obligations

The optional model includes a contextual conversion to bool that reports presence. This lets a condition ask whether there is a value before the program uses it. The test says nothing about the truth value of an integer contained inside. An optional containing zero is present.

The distinction can be shown without a separate sentinel convention. In the following fragment, the Boolean test concerns whether entry_at returned a value, and dereferencing obtains the contained integer only on that path.

int value_or_zero(const std::vector<int>& values, std::size_t position)
{
    auto result = entry_at(values, position);
    if (result) {
        return *result;
    }
    return 0;
}

This wrapper intentionally collapses absence and a present zero in its own return type. That may be an appropriate policy for this caller, but the original function did not force the policy on every caller. The richer result type leaves the decision at the place that knows whether the distinction matters.

The draft also specifies value(), which throws bad_optional_access when the optional is empty. That supplies a defined checked access policy. value_or supplies another policy, returning the contained value when present and a converted fallback otherwise. These operations should be chosen according to the intended handling of absence, not treated as identical spellings.

Copy and move behavior follows the contained type's capabilities. The draft deletes copying when the contained type is not copy-constructible, and moving constructs the contained value from the source's moved value when present. The optional wrapper does not make an otherwise uncopyable object copyable simply because it records an empty state.

Moving an optional also does not automatically make the source empty. N4659 explicitly says that the source's presence state is unchanged by the move constructor. If it contained a value, that value has participated in a move, with the consequences defined by its own type. The distinction prevents a caller from assuming that an optional transfers presence in the same way a particular owning pointer might transfer ownership. An empty-state flag and a moved-from contained object describe different facts. Code that needs to clear the source can express that action separately through the optional interface, rather than relying on an unstated side effect of moving.

The revision distinguishes other interface roles too

The change catalogue places optional beside variant and any, but they describe different value models. A variant represents a selected alternative from a specified set of types. An any object type-erases a copyable value and allows a caller to ask for a value of a particular type. Absence of a single known type is the narrower question that optional answers.

Likewise, string_view gives an interface a non-owning view of string contents. The catalogue presents it for functions that need to inspect a string without owning or modifying it. That is a different lifetime agreement from an optional that contains and manages a value object. Similar convenience at the call site does not make their ownership properties interchangeable.

These vocabulary types are useful because their contracts travel with the declaration. A project can define the same distinctions through local conventions and custom wrappers, but a standard-library type gives separately written code a shared starting point. The application still decides which distinction belongs in each interface.

Approval completes a standards decision, not a deployment

Sutter's report makes the remaining publication work editorial. It does not present September's result as another opportunity to change the technical design. The absence of technical ballot comments and unanimous vote are the specific facts that permit the shortened approval path described in the report.

Implementation remains a separate matter. The standard defines the facility, while a selected compiler and library determine whether a program can use it in its actual build environment. A claim that a toolchain supports C++17 needs to be assessed at the level of the features the program depends on, including library facilities such as optional.

I find the value vocabulary a substantial part of this revision's practical benefit. An interface can distinguish an absent object from a present zero and can state that the returned value owns its contained storage. The wrapper then manages construction and destruction under a documented model. Unanimous approval settles the revision's major ballot, while those concrete lifetime and presence rules explain what the completed design offers to ordinary application code.