Adding parameters to an object can be messy. Martin Moene demonstrates how method chaining can make code more readable.
For a new kind of measurement in our application for scanning probe microscopy [ Wikipedia-a ], I need to construct a curve that consists of several kinds of segments. The curve can for example describe the movement of the tip perpendicular to the surface of the material investigated and what data shall be acquired.
Behaviour of segment types varies. One segment may describe how the surface is approached, another how to move away from the surface and yet another describes a dwell time. Such a curve is part of force-distance spectroscopy [ Wikipedia-b ]. Figure 1 below shows what the researchers would like to do.
Figure 1 |
To a large extent the structure of a curve is fixed and this structure can be created at compile-time. Some variation is required at run-time, which can be arranged for via parameters. The simplified code in Listing 1 illustrates this.
#include "curve.hpp" int main() { // run-time configurable: const int Nsweep = 1; const bool skipR1 = false; auto scanner = create_scanner ( "Z" ); const auto distance = create_condition( "123 nm" ); const auto threshold = create_condition( "chan1", "<=", "2.7 V" ); Curve curve; curve.times( Nsweep ) .scans( scanner ) .add ( Retract ().stop_on( distance ) ).unless( skipR1 ) .add ( Approach().stop_on( threshold ) ) .add ( Retract ().stop_on( distance ) ) ; std::cout << "curve.sweep(): "; curve.sweep(); } |
Listing 1 |
The curve built describes that: Nsweep times, scanner
Z
retracts the tip from the material surface for 123 nm (unless skipped), then approaches the surface until the measured value of
chan1
reaches the threshold value of 2.7 V, and finishes with retracting 123 nm again (t1 and t2 are omitted). Note that the threshold condition depends on other information than the scan distance. It can have any unit that makes sense in the experiment.
In the real implementation, there are many more parameters. To keep it simple, several things such as data acquisition are omitted here. Another simplification is to use struct without access specifiers for all classes in the code.
Compiling and running the simplified program gives:
prompt>g++ -Wall -Wextra -Weffc++ -std=c++11 \ -o curve.exe curve.cpp && curve curve.sweep(): RZL AZ<= RZL
The output
RZL AZ<= RZL
indicates the type of segment, scanner and condition used for each segment:
R
for Retract,
A
for Approach,
Z
for ZAxisScanner,
<=
for LessEqualCondition and
L
for LengthCondition.
Fluent interface
The construct sketched above evolved from the following (simplified) C++98 code (Visual C++ 6).
return CurveDefinition(). sweep( sweepCount ). add( CurveSegmentPtr( new SweepCurveSegment ( scanner, condFalse, ... ) ) ). add( CurveSegmentPtr( new SweepCurveSegment ( scanner, condition, ... ) ) ). add( CurveSegmentPtr( new SweepCurveSegment ( scanner, condFalse, ... ) ) );
Although the sketched curve may be adequate for many kinds of experiments, it is a simplification of a more general approach. Experience with an initial version of the code led the researchers to express several additional wishes. For example to be able to conditionally include a segment, to only perform it once, or to perform a collection of segments multiple times.
As you see, the new code expands on the use of method chaining [
Wikipedia-d
], a key element of a fluent interface [
Wikipedia-e
]. Method chaining is also known as the named parameter idiom. In addition to method chaining, other variations are imaginable, such as function-like modifiers. For example to include a segment in the first sweep only with
once( segment )
or to perform a collection of segments (a sub-curve or section) a number of times via
times( N, section )
.
The new code also moves the allocation of segments out of the fluent interface. This makes the code much more readable. It also leads to the main subject of this article: static polymorphic named parameters.
Thus, the reason to compose the curve in this way is to benefit from a clear and flexible notation. As an internal domain-specific language [ Fowler08 ] it also helps researchers to recognise the curve they sketched in the code.
Static polymorphism
Now, let’s examine how a curve is constructed. Curve’s method
add()
creates dynamic segment objects from the non-dynamic temporary ‘exemplars’. To create a dynamic copy of the segment of the original type,
add()
is templated.
template< typename T > Curve & Curve::add( T const & segment ) { segments.emplace_back( std::make_unique<T>( segment ) ); return *this; }
Note:
std::make_unique<T>()
is a C++14 feature [
make_unique
].
Looking at above code, it becomes clear that a call like
Approach().stop_on(...)
must itself return an object of type
Approach
to add the right type of segment to the curve.
Here is the crux of this article. Do all types such as
Approach
require their own method
stop_on()
to return the appropriate type? Fortunately that’s not the case, thanks to the curiously recurring template pattern or CRTP. See [
Wikipedia-e: Subclasses
] and [
Wikipedia-f
] respectively. (See Listing 2.)
template <typename Derived> struct SegmentParameter : SegmentCommon { #define self crtp_cast<Derived>(*this) Derived & stop_on( ConditionPtr cond ) { condition( cond ); return self; } #undef self }; struct Approach : SegmentParameter<Approach> { // inherited: // Approach & stop_on( ConditionPtr s ); }; |
Listing 2 |
With this construct
Approach
can inherit
stop_on()
that returns the desired
Approach &
instead of
SegmentParameter &
. The
crtp_cast
combined with a macro enables us to write
return self
to return the current object
with the right type
where we would otherwise write
return *this
[
Bendersky11
]. The shortest of four
crtp_cast
const-volatile variations is:
template<class D, class B> D & crtp_cast(B & p) { return static_cast<D &>( p ); }
For an interesting discussion about encapsulation and the CRTP, see Better Encapsulation for the Curiously Recurring Template Pattern by Alexander Nasonov [ Nasonov05 ].
Build to use
In the end we’ve built a curve that contains a collection of smart-pointered segments that originate in interface
Segment
. At the same time the curve is a segment itself, so that it can act as a sub-curve or section of another curve. See the following derivation chains.
Curve → SegmentParameter<Curve> → SegmentCommon → Segment Approach → SegmentParameter<Approach> → SegmentCommon → Segment ...
Thus, whereas construction of the curve builds on automatic ‘exemplar’ objects and static polymorphism via the CRTP compile-time technique, using the curve occurs via classical dynamic polymorphism with
Segment
as the interface.
Letting go of the garbage
One little thing worries me: the code for the sequence
curve.add(...).unless(...)
is both elegant and inelegant at the same time. It is simple, but then it lets you create a segment to only throw it away immediately via
unless()
.
Curve & unless( bool skip ) { if ( skip ) segments.pop_back(); return *this; }
One can argue that recycling is good and more garbage means more recycling, but that isn't entirely in line with Bjarne Stroustrup’s idea [ Kalev13 ]:
So, I say that C++ is my favorite GC language because it generates so little garbage.
As a reviewer pointed out, one may circumvent the awkward situation by prefixing the condition, or by including it in a conditional add function like so:
curve.enable_if_not( skipR1 ) .add( Retract ().stop_on( distance ) ); curve.add_if_set( performR1, Retract ().stop_on( distance ) );
However, to ease reading of consecutive lines, I’d prefer to keep the left part of the lines similar, as illustrated here.
curve.add_if( Retract ().stop_on( distance ), performR1 ) .add ( Approach().stop_on( threshold ) );
We’re leaving the realms of fluid interfaces and named parameters though.
Summary
In this article a fluent interface is applied to a simplified setting for scanning probe spectroscopy. The resulting code shows a close relationship to the graphic representation provided by the researchers. The main point of the article is to show how one can obtain the static inheritance required for named parameters in this setting via the curiously recurring template pattern.
Acknowledgements
I’d like to thank the Overload team for reviewing the article and Jonathan Wakely for clarifying several aspects of smartpointers. Their remarks and suggestions were key in improving the article.
Notes and references
Code for this article and code for a larger example with modifiers
once
and
times
is available on [
GitHub
].
[Arena12] Use CRTP for polymorphic chaining. Marco Arena. 29 April 2012. http://marcoarena.wordpress.com/2012/04/29/use-crtp-for-polymorphic-chaining/ (Presents a slightly different application of the CRTP.)
[Bendersky11] The Curiously Recurring Template Pattern in C++. Eli Bendersky. 17 May 2011.
http://eli.thegreenplace.net/2011/05/17/the-curiously-recurring-template-pattern-in-c/
(Mentions
crtp_cast
in a comment.)
[Fowler05] Fluent Interface. Martin Fowler. 20 December 2005. http://www.martinfowler.com/bliki/FluentInterface.html
[Fowler08] Domain-Specific Language. Martin Fowler. 15 May 2008. http://martinfowler.com/bliki/DomainSpecificLanguage.html
[GitHub] Code for Static polymorphic named parameters in C++. Martin Moene. 31 December 2013. https://github.com/martinmoene/martin-moene.blogspot.com/tree/master/Static polymorphic named parameters in C++
[K-ballo13] Episode Eight: The Curious Case of the Recurring Template Pattern. Tales of C++ K-ballo . 2 December 2013. http://talesofcpp.fusionfenix.com/post-12/episode-eight-the-curious-case-of-the-recurring-template-pattern
[Kalev13] An Interview with Bjarne Stroustrup. Danny Kalev and Bjarne Stroustrup. May 15, 2013. http://www.informit.com/articles/article.aspx?p=2080042
[make_unique] make_unique.
CppReference
.
http://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique
Here, function
make_unique<>()
is equivalent to
std::unique_ptr<T>(new T(std::forward<Args>(args)...))
. See also Herb Sutter’s GotW #89 Solution: Smart Pointers (
http://herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers/
) and GotW #102: Exception-Safe Function Calls (
http://herbsutter.com/gotw/_102/
).
[Nasonov05] Better encapsulation for the curiously recurring template pattern. Alexander Nasonov. Overload , 70:11-13, December 2005 ( http://accu.org/index.php/journals/296 ).
[ Wikipedia-a] Scanning Probe Microscopy. Wikipedia . http://en.wikipedia.org/wiki/Scanning_probe_microscopy Accessed 21 December 2013.
[Wikipedia-b] Force-Distance spectroscopy . Wikipedia . http://en.wikipedia.org/wiki/Atomic_force_microscopy#Force_spectroscopy Accessed 19 December 2013. See also [ Wikipedia-c ].
[Wikipedia-c] Scanning tunneling spectroscopy. Wikipedia . http://en.wikipedia.org/wiki/Scanning_tunneling_spectroscopy Accessed 19 December 2013.
[Wikipedia-d] Method chaining or named parameter idiom. Wikipedia . http://en.wikipedia.org/wiki/Method_chaining Accessed 16 December 2013. See also [Wikipedia-e].
[Wikipedia-e] Fluent interface. Wikipedia . http://en.wikipedia.org/wiki/Fluent_interface Accessed 21 December 2013. See also [ Fowler05 ]
[Wikipedia-f] Curiously recurring template pattern (CRTP). Wikipedia . http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern Accessed 17 December 2013. See also [ K-ballo13 ], [ Bendersky11 ], [ Arena12 ].