March 18, 2018·4 min read·engineeringcppstandardsrangeslookup

Jacksonville relaxes range-for lookup around unrelated members

Jacksonville's range-for correction lets an ADL begin/end pair remain usable when a class has only one of those member names. The meeting's Evolution report separates this adopted correction from larger features still under discussion.

An unrelated member named end can prevent a type from participating in a range-for loop, even when suitable nonmember begin and end functions exist. Jacksonville's Evolution status report records a correction to that rule. Ville Voutilainen's March 18 report, P1018R0, says P0962R0 moved as a defect report at the meeting. The change requires finding both member names before committing range-for to the member-function path.

WG21's meeting agenda places Jacksonville on March 12 through 17. Its primary goals include work on C++20 features and resolving Modules TS comments. The status report gives a useful account immediately after that work, distinguishing adopted corrections from proposals that still need design or wording review.

The lookup correction interests me because reading C++ often requires knowing rules that are distant from the source expression. A range-for loop looks simple, but its choice of customization mechanism can depend on an inherited name that was never intended to describe iteration. Fixing that interaction reduces an accidental dependency without adding another runtime abstraction.

Finding one name used to commit the whole lookup

Voutilainen's P0962R0, dated February 11, explains the problem. Under the rule it seeks to change, finding either begin or end in the class commits the loop to member access for both endpoints. The other member need not exist. Nor does the member that was found necessarily represent the intended range interface.

The paper's example derives a wrapper from std::stringstream and supplies nonmember iterator functions. Lookup nevertheless finds the stream hierarchy's end enumerator associated with seeking. That unrelated name selects the member path, so the nonmember iterator pair cannot provide range-for traversal.

The important point is the name collision, not a failure of the supplied iterator functions. The programmer deliberately provided the nonmember pair, but the loop's customization selection excludes it before it can perform the intended job. An inherited declaration becomes an obstacle to expressing a separate interface.

P0962R0 proposes a narrower trigger. Both member names must be found to select the member path. If only one appears, lookup proceeds to the argument-dependent nonmember mechanism. It does not combine a member begin with a nonmember end, or the reverse. The endpoint pair still comes from one selected mechanism.

The stream wrapper illustrates the difference

A shortened version of the proposal's example gives the wrapper a named type and places the free functions in its associated namespace:

#include <iterator>
#include <sstream>

struct input : std::stringstream {};

std::istream_iterator<char> begin(input& stream)
{
    return std::istream_iterator<char>(stream);
}

std::istream_iterator<char> end(input&)
{
    return std::istream_iterator<char>();
}

void consume(input& stream)
{
    for (char value : stream)
        (void)value;
}

This is the case the correction enables. It is not a claim that every compiler already implements the defect resolution. The example also does not make a standard stream directly iterable by itself. It supplies the customization functions for a user-defined wrapper and lets the corrected selection rule reach them.

An alternative wrapper without stream inheritance could avoid the inherited name under the previous rule. Voutilainen discusses that workaround but notes a semantic cost. A derived stream wrapper retains its standard conversion to the stream base. An unrelated wrapper introduced solely for iteration does not retain that same relationship automatically.

The correction therefore preserves an intended type relationship while removing the accidental name-based obstacle. It does not settle whether inheriting from a stream is the preferred design in every application. That broader judgement is separate from whether a range-for customization can be expressed for a type that already has the relationship.

The earlier rule tried to prevent a different surprise

P0962R0 traces the existing rule to N3257 and the closing work on range-for in 2011. The motivating concern was a class with an incomplete member customization unexpectedly falling back to namespace-scope templates. Selecting the member path early could make that mistake ill-formed instead of finding an unintended nonmember pair.

Voutilainen argues that this protection also blocks deliberate customizations where the programmer cannot remove an inherited name. The revised trigger changes that balance. It keeps a complete member pair authoritative while allowing the nonmember mechanism when the class lacks one of the two names.

I favor this kind of correction because it addresses a specific interaction with existing code. The new rule can be stated in terms of the declarations found by lookup. It gives the author of a wrapper a usable customization path without requiring a new object representation merely to escape one inherited enumerator.

Larger Jacksonville work has different status

P1018R0 records related structured-binding lookup work moving as defect reports. It also describes larger features with less finality. The modules discussion aims at merging proposed changes into the Modules TS design, with a future proposal expected. Coroutine design work is expected to continue, even as particular changes move into the Coroutines TS.

The structured-binding correction has a similar selection problem. The report says P0961R0 requires the relevant member get to be a template before the member customization is selected. That lets wrappers involving smart pointers or tuples participate in decomposition without an unrelated member taking precedence. Another correction permits bindings to accessible members instead of requiring that the members be public. Accessibility can depend on the context in which the binding appears, so the broader permission does not mean that every caller gains access to every member. These changes adjust how existing declarations interact with compact language syntax, which is also the issue behind the range-for correction.

Those distinctions matter when reading a meeting summary as an implementation guide. Evolution approval, movement into a TS and adoption of a defect correction do not promise the same thing. The report explicitly says some proposals need further work or lack a designated publication vehicle. A list of topics discussed at Jacksonville cannot be treated as a list of completed C++20 facilities.

For range-for, the report provides a concrete outcome tied to specific wording. Member lookup needs both endpoint names before selecting member access. Otherwise, the nonmember customization remains available through argument-dependent lookup, including for a wrapper whose hierarchy contains an unrelated end declaration.