10

Tom Honermann, C++ minion

 3 years ago
source link: http://honermann.net/blog/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Tom Honermann, C++ minion – ++Thoughts on C++

In Why Concepts didn’t make C++17 [Twitter][Reddit], I summarized a number of technical concerns that were raised during the C++ standard committee meeting in Jacksonville in March, 2016. In this next series of posts, I’ll be discussing #6 on that list; separate checking of constrained template definitions.

I’ve been reviewing some of the original Concepts proposal papers [N1510][N1522][N1536] dating back to 2003 and have been struck by two observations.  1) How true the current Concepts proposal is to those original proposals, and 2) How much those original proposals emphasized the benefits of separate checking.  After re-reading these, it feels to me that gaining Concepts as currently proposed will have lost much of the promise of these initial papers if support for separate checking never arrives.

A quick refresher: separate checking refers to the ability of the compiler to concept check a constrained template definition, without concrete template arguments, to validate that the template definition does not make use of template parameters in a way that violates the declared constraints.  Ideally, a fully constrained template definition that the compiler has successfully concept checked would be guaranteed not to fail type checking when instantiated with any set of template arguments that satisfy the declared constraints.

The C++0x Concepts proposal included separate checking.  Ultimately, that proposal failed to reach consensus due to usability and implementability (primarily performance) concerns and, after enjoying a brief stint as an accepted feature of C++0x, was withdrawn before C++11 shipped.  A number of papers and articles have been written about the challenges with the C++0x proposal.  Of the ones I’ve read, I’ve found N2906 [N2906] and an article in Dr. Dobb’s by Bjarne Stroustrup [Dr.Dobb’s] to be the most informative.

One of the separate checking concerns that is often raised is how to grant implementation freedom to constrained template authors to allow for template argument dependent debugging, logging, tracing, telemetry, caching, etc… without having to pollute the constraint declarations of the template interface with requirements specific to the implementation of those facilities.  This issue was raised again in an email thread [SG8-Concepts] on the SG8 Concepts reflector just prior to the Jacksonville meeting in response to Matt Calabrese’s paper [P0240R0].  As an example, consider a hypothetical implementation of std::copy that supports a debug mode enabling validation that the input range provided is a valid range; that last is reachable from first:

    template<InputIterator II, OutputIterator OI>
    OI copy(II first, II last, OI result) {
    #if DEBUG_MODE_ENABLED
      validate_range(first, last);
    #endif
      ...
    }

The call to validate_range is not supported by the InputIterator requirements, so this implementation would presumably fail a concept check when compiled with debug mode enabled (depending on how strict definition checking is and how validate_range is defined).  The C++0x Concepts proposal [N2773] included support for suspending concept checking at the block level via the late_check statement in order to support cases like this.  I was not present for any of the C++0x Concepts discussions, but my understanding is that late_check was rather unloved given that it existed only to undermine the definition checking that C++0x Concepts was designed to do.  Nevertheless, it was accepted as a necessary evil.   Can we do better?  Could a different design fit more naturally into the current Concepts design?

The approach I favor is one inspired by the C++17 if constexpr feature and independently suggested by myself on the EWG reflector [isocpp-lib-ext] and by Nicol Bolas on the std-proposals list [std-proposals].  The idea is to introduce an if requires statement that enables a template author to relax constraints at a block level based on whether additional requirements are met.  When concept checking a block guarded by an if requires statement, the compiler would proceed as though the additional constraints were appended to the conjunction of associated constraints for the template declaration.  The example above might be adapted to use it as follows.

    template<typename I>
    concept bool SupportsRangeValidation =
      InputIterator<I> &&
      requires (I first, I last) {
        validate_range(first, last);
      };

    template<InputIterator II, OutputIterator OI>
    OI copy(II first, II last, OI result) {
    #if DEBUG_MODE_ENABLED
      if requires(SupportsRangeValidation<II>) {
        validate_range(first, last);
      }
    #endif
     ...
    }

The grammar for this statement would be an addition to selection-statement:

    selection-statement:
      if requires-clause statement
      if requires-clause statement else statement

The use of requires-clause is intended to maintain consistency with constraint declarations, but does mean that adding constraints via a requires expression will require the unfortunate "requires requires" syntax.  Presumably, any solution adopted to address the existing "requires requires" issue would be applicable here as well.

But wait, constrained function templates can be overloaded and partially ordered based on constraint refinement.  Does the following not address the use case above?  Why would we need if requires?

    template<InputIterator II, OutputIterator OI>
    OI copy(II first, II last, OI result) {
      ...
    }

    #if DEBUG_MODE_ENABLED
    template<InputIterator II, OutputIterator OI>
      requires SupportsRangeValidation<II>
    OI copy(II first, II last, OI result) {
      validate_range(first, last);
      ...
    }
    #endif

The above does work, but I find it deeply unsatisfying as it means the common parts of both implementations must either be duplicated or factored out. Additionally, as discussed in my prior post, the additional overload would contribute to longer error messages and, given that the overload only exists to satisfy implementation details, leaks concerns that users presumably would rather not be bothered with.  This has the feel to me of working around language limitations.  The if requires approach feels like a much more integrated, elegant, and simple solution.

Does if requires suffice to address the needs for escaping constraints?  I don’t think so.  Authors of existing unconstrained templates, may wish to adopt interface constraints as an alternative to std::enable_if, or to improve error messages for users, without having to go through the trouble of defining concepts like SupportsRangeValidation above.  Presumably, their existing templates are working just fine for the types that they are actually instantiated with in practice, even if those templates use expressions that aren’t required by their interface guarantees.  I think it makes sense to support a mechanism to opt out of constraint checks completely in order to enable such adoption.  I favor a requires ... statement for these cases as follows.  Note that this is just the C++0x Concepts late_check statement dressed in a different syntax, so all of the objections previously raised against late_check are relevant here as well.

    template<InputIterator II>
    void f(II i) {
      requires ... {
        // unconstrained block...
      }
    }

Allowing a requires ... statement to wrap a function body like a function-try-block would enable constructor templates to opt out of constraint checking for base class and data member initializers.

    template<InputIterator II>
    my_class(II i)
    requires ...
      : i{i} // unconstrained initializers...
    {
      // unconstrained constructor body...
    }

I think if requires and requires ... statements suffice to address relaxation of constraints within function templates.  If more fine-grained relaxation is found to be necessary, then the requires ... syntax can be extended to enable relaxing constraints on expressions and initializers.  But, what about class, variable, and alias templates? Is there a need to relax constraints for base class specifiers or member function, data member, variable, or alias declarations?  The use cases I’ve come across so far I think can be adequately addressed through class inheritance and partial ordering of constrained explicit specializations, though I admit the refactoring required could be extensive.  The following prototype grammar includes support for fairly fine grained constraint relaxation.  (Note that I’m cheating here by applying opt to the combined requires ... token sequence).

    primary-expression:
      requires... expression

    selection-statement:
      if requires-clause statement
      if requires-clause statement else statement

    compound-statement:
      requires...opt { statement-seqopt }

    function-try-block:
      requires...opt try ctor-initializeroptcompound-statement handler-seq

    alias-declaration:
      using identifier attribute-specifier-seqopt = requires...opt defining-type-id ;

    braced-init-list:
      requires...opt { initializer-list ,opt }
      requires...opt { }

    class-specifier:
      class-head requires...opt { member-specificationopt } }

    base-clause:
      : requires...opt base-specifier-list

The concerns regarding separate checking expressed at the Jacksonville meeting had to do with the ability to provide separate checking as an incremental addition on top of the current Concepts proposal at some future date. With regard to the issues explored in this post, it looks to me like adequate solutions can be provided without requiring changes to the current proposal or compromising code written to it. However, I think there are other issues that are of greater concern. More on that next time.

References:

[N1510] Bjarne Stroustrup, “Concept checking – A more abstract complement to type checking”, N1510, 2003.
http://open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1510.pdf [N1522] Bjarne Stroustrup and Gabriel Dos Reis, “Concepts – Design choices for template argument checking”, N1522, 2003.
http://open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1522.pdf [N1536] Bjarne Stroustrup and Gabriel Dos Reis, “Concepts – syntax and composition”, N1536, 2003.
http://open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1536.pdf [N2773] Douglas Gregor et al., “Proposed Wording for Concepts (Revision 9)”, N2773, 2008.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2773.pdf [N2906] Bjarne Stroustrup, “Simplifying the use of concepts”, N2906, 2009.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2906.pdf [P0240R0] Matt Calabrese, “Why I want Concepts, but why they should come later rather than sooner”, P0240R0, 2016.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0240r0.html [Dr.Dobb’s] Bjarne Stroustrup, “The C++0x “Remove Concepts” Decision”, 2009.
http://www.drdobbs.com/cpp/the-c0x-remove-concepts-decision/218600111 [SG8-Concepts] Faisal Vali et al., “Is there really no path forward for definition checking of templates?”, 2016.
https://groups.google.com/a/isocpp.org/forum/#!msg/concepts/CX3aUunonaI/D6-jtlkfFgAJ [isocpp-lib-ext] Tom Honermann et al., “constexpr if and concept definition checking”, 2016.
EWG reflector, email thread titled “constexpr if and concept definition checking”, 2016-02-26 [std-proposals] Nicol Bolas, “if requires: the concepts-based if constexpr”, 2016.
https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/8KgGLq50RLY

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK