May 3, 2019·6 min read·engineeringcppgccdiagnosticstemplates

GCC 9 simplifies conditional constructors and diagnoses wasted moves

GCC 9.1 implements experimental C++20 mechanisms while adding diagnostics for lifetime mistakes and move expressions that obstruct existing language rules.

A library wrapper sometimes needs one constructor that permits implicit conversion for some template arguments and requires explicit construction for others. Earlier C++ code can implement that policy with two mutually exclusive declarations. GCC 9.1, released May 3, implements experimental support for explicit(bool), which lets the declaration express the condition itself.

The GCC 9 changes also list diagnostics for unnecessary moves and dangling initializer lists. These changes address a related difficulty in C++ source. Extra syntax can look like additional control while duplicating or obstructing behavior the language already supplies.

I find C++ most persuasive when it expresses ownership and produces code without an abstraction's runtime overhead. Its cost is the number of rules a reader must reconstruct. Conditional explicitness is interesting because it can remove declarations whose only purpose is to encode one property of a constructor.

One constructor can describe a conditional policy

Barry Revzin and Stephan T. Lavavej's P0892R2 explains why library types need conditional explicitness. A wrapper should often preserve whether constructing its contained type requires an explicit conversion. The paper names common utility types including pair and optional as examples.

The previous technique uses separate constructor templates with disjoint conditions. Their bodies perform the same initialization, while their declarations differ in whether they contain explicit. A reader has to recognize that the pair of declarations implements one policy.

The new syntax places a Boolean constant expression inside explicit. A true condition makes the constructor explicit, while false leaves it eligible for implicit conversion. A simple experimental example isolates that rule:

template<bool RequireExplicit>
struct number {
    int value;

    explicit(RequireExplicit) number(int input) : value(input) {}
};

number<false> implicit_number = 4;
number<true> explicit_number(4);

The two specializations share the constructor's implementation. Their difference is the permitted initialization context. Copy-initializing number<true> from an integer would fail, while direct initialization remains available.

A real wrapper can base the condition on a convertibility trait for its underlying value. The constructor still needs a separate condition describing whether construction is possible at all. Explicitness and participation in overload resolution answer different questions. Combining them into one undifferentiated constraint would make the interface harder to reason about.

The paper retains this separation in its examples. A constructibility requirement controls whether a constructor is usable, and the explicit condition describes whether the conversion can happen implicitly. Moving the latter into language syntax removes a repeated declaration without changing the reason for the former.

Experimental mode has a specific meaning

GCC enables its developing C++20 implementation through -std=c++2a or the GNU extension variant -std=gnu++2a. The release notes call this support experimental and list individual implemented capabilities. Selecting the mode does not establish that every proposal for the upcoming standard is present or final.

That distinction is important for library authors experimenting with explicit(bool). The language feature can simplify an implementation while the supported compiler set still determines whether the public headers can use it. A short declaration in an experimental mode does not automatically replace an implementation needed by older language modes.

The release also implements class types as non-type template parameters. The originating P0732R2 paper explains the inconsistency it aims to remove. Constant evaluation can produce class values, yet the earlier template parameter rules cannot accept those values in the same way they accept certain scalar values.

This addition concerns template identity as well as syntax. A compiler has to decide when two arguments name the same specialization and how to represent them for linking. The paper therefore constrains the allowed classes and relates identity to their component values. Merely having a constexpr constructor does not imply that every class becomes an unrestricted template argument.

A move can prevent construction from being elided

GCC 9 introduces -Wpessimizing-move, enabled by -Wall. The versioned warning manual describes it as diagnosing move expressions that prevent copy elision. This addresses a common misunderstanding of what std::move requests.

A return of a named local object can be eligible for named return value optimization. Wrapping that object in std::move changes the expression and can prevent that optimization. The more explicit-looking source can therefore require work that the simpler return allowed the implementation to omit.

The following example illustrates the source pattern the warning is meant to question. The function's local result is the object being returned. Its name already gives the return statement the form relevant to named return value optimization:

#include <string>
#include <utility>

std::string label()
{
    std::string text = "ready";
    return std::move(text);
}

Returning text directly expresses the value transfer while retaining the relevant elision opportunity. This does not imply that every use of std::move is unnecessary. Moving a member or a value whose lifetime and expression category differ requires its own analysis.

GCC separately introduces -Wredundant-move under -Wextra. A redundant move and a pessimizing move describe different reasons to remove syntax. One can repeat a conversion already supplied by return rules, while the other interferes with an elision opportunity.

I value diagnostics that name this difference. Generic advice to add moves when returning expensive objects ignores the language's existing return behavior. A compiler can identify the particular expression and tell the programmer whether the apparent optimization changes anything useful.

An initializer list does not own an independent array

The new -Winit-list-lifetime warning is enabled by default. The release describes cases such as returning or assigning from a temporary initializer list that leave a dangling pointer. A lightweight object naming elements is different from an owning container whose storage follows the object's lifetime.

An unsafe function can be very short. The return type does not make the elements survive the return. Here the result names storage whose lifetime will not cover the caller's later iteration:

#include <initializer_list>

std::initializer_list<int> invalid_values()
{
    return {1, 2, 3};
}

The problem is the lifetime of the underlying array. Copying the initializer_list representation does not independently allocate or retain those elements. A consumer that later iterates the result needs storage that still exists.

That is a useful warning for a language often praised for value semantics. A value-looking wrapper can contain references to storage owned elsewhere. The reader must distinguish the wrapper's own lifetime from the lifetime of its referents. A diagnostic at the return expression exposes where those lifetimes diverge.

An owning result type is appropriate when the function intends to return elements that survive independently. The choice of owning container depends on the interface's size and allocation requirements. The compiler warning identifies the invalid lifetime assumption, while the replacement type states the intended ownership.

Library changes affect ordinary C++17 code

The release notes say libstdc++'s C++17 implementation is no longer experimental. They also state that using filesystem no longer requires linking with -lstdc++fs. These changes concern the shipped library and linking interface rather than only the experimental language mode.

That separation matters in a compiler upgrade. An application can remain in a C++17 mode while gaining a different library integration requirement. Conversely, accepting a C++20 syntax extension does not prove that every associated library facility exists. The frontend and standard library provide different parts of the development environment.

The release also adds parallel algorithm support with a stated dependency on Thread Building Blocks 2018 or newer. A standard library declaration can therefore have a runtime or build dependency that the language mode does not describe. Compiler support tables need to be read alongside the library's requirements.

Diagnostics preserve the relevant source context

GCC 9 also improves the locations retained for literals and initialization expressions. The release notes show errors pointing at the problematic initializer rather than only a closing brace. That difference matters when several initializers occupy the same declaration and only one has an incompatible type.

Its diagnostics can label operand regions, and JSON output can preserve groups of related messages for other tools. A call-site error can then remain associated with the declaration that explains the required parameter. This reporting work complements new warnings. Detecting a questionable construct is useful only if the message gives a reader enough context to understand which rule the source has violated.

The source should express the missing decision

GCC 9's conditional explicitness support and move warnings both reward a more exact description of intent. A constructor needs a condition governing implicit conversion. A return statement often already contains the information needed to move or elide an object without an additional cast.

The initializer-list warning makes the same demand about ownership. The type and expression must provide storage with the lifetime the caller expects. Across these changes, the useful question is which semantic decision remains unstated. Explicit(bool) supplies a decision that previously required repeated declarations, while the new diagnostics identify expressions whose extra syntax or lightweight representation can mislead a reader about the work and lifetime involved.