📄 phoenix_users_manual.txt
字号:
member_var_ptr<int, xyz> xyz_v = &xyz::v;
The template parameter of the member_var_ptr is the type of the variable followed by the class:
int xyz::v; ---> member_var_ptr<int, xyz>
Just like the member_function_ptr, member_var_ptr also expects the first argument to be a pointer or reference to an object. Both the object (reference or pointer) and the arguments can be lazily bound. Like member function binders, var(obj) must be used to access non-const member variables. Examples:
xyz obj;
xyz_v(arg1) // arg1.v (const& access)
xyz_v(obj) // obj.v (const& access)
xyz_v(var(obj))() = 3 // obj.v = 3 (non-const& access)
xyz_v(arg1)(obj) = 4 // obj.v = 4 (non-const& access)
Be reminded once more that binders are monomorphic. This layer is provided only for compatibility with existing code such as prewritten STL functors and legacy APIs. Rather than binding functions or functors, the preferred method is to write true generic and polymorphic lazy-functions (see functions). However, since most of the time we are dealing with adaptation of exisiting code, binders are indeed indespensible.
[page:1 Adaptable closures]
The framework will not be complete without some form of closures support. Closures encapsulate a stack frame where local variables are created upon entering a function and destructed upon exiting. Closures provide an environment for local variables to reside. Closures can hold heterogeneous types.
Phoenix closures are true hardware stack based. Closures enable true reentrancy in lambda functions. A closure provides access to a function stack frame where local variables reside. Modeled after Pascal nested stack frames, closures can be nested just like nested functions where code in inner closures may access local variables from in-scope outer closures (accessing inner scopes from outer scopes is an error and will cause a run-time assertion failure).
[blurb __detail__ [*'''Spirit''' Closures][br][br]Spirit uses Phoenix closures to allow parameter passing (inherited and synthetic attributes, in parsing parlance) upstream and downstream in a parse traversal (see spirit_docs documentation).]
There are three (3) interacting classes:
[*1) closure:]
At the point of declaration, a closure does not yet create a stack frame nor instantiate any variables. A closure declaration declares the types and names of the local variables. The closure class is meant to be subclassed. It is the responsibility of a closure subclass to supply the names for each of the local variable in the closure. Example:
struct my_closure : closure<int, string, double> {
member1 num; // names the 1st (int) local variable
member2 message; // names the 2nd (string) local variable
member3 real; // names the 3rd (double) local variable
};
my_closure clos;
Now that we have a closure 'clos', its local variables can be accessed lazily using the dot notation. Each qualified local variable can be used just like any primitive actor (see primitives). Examples:
clos.num = 30
clos.message = arg1
clos.real = clos.num * 1e6
The examples above are lazily evaluated. As usual, these expressions return composite actors that will be evaluated through a second function call invocation (see operators). Each of the members (clos.xxx) is an actor. As such, applying the operator() will reveal its identity:
clos.num() // will return the current value of clos.num
[blurb __note__ [*Acknowledgement:][br][br][*Juan Carlos Arevalo-Baeza] (JCAB) introduced and initilally implemented the closure member names that uses the dot notation and [*Martin Wille] who improved multi thread safety using Boost Threads.]
[*2) closure_member]
The named local variables of closure 'clos' above are actually closure members. The closure_member class is an actor and conforms to its conceptual interface. member1..memberN are predefined typedefs that correspond to each of the listed types in the closure template parameters.
[*3) closure_frame]
When a closure member is finally evaluated, it should refer to an actual instance of the variable in the hardware stack. Without doing so, the process is not complete and the evaluated member will result to an assertion failure. Remember that the closure is just a declaration. The local variables that a closure refers to must still be instantiated.
The closure_frame class does the actual instantiation of the local variables and links these variables with the closure and all its members. There can be multiple instances of closure_frames typically situated in the stack inside a function. Each closure_frame instance initiates a stack frame with a new set of closure local variables. Example:
void foo()
{
closure_frame<my_closure> frame(clos);
/* do something */
}
where 'clos' is an instance of our closure 'my_closure' above. Take note that the usage above precludes locally declared classes. If my_closure is a locally declared type, we can still use its self_type as a paramater to closure_frame:
closure_frame<my_closure::self_type> frame(clos);
Upon instantiation, the closure_frame links the local variables to the closure. The previous link to another closure_frame instance created before is saved. Upon destruction, the closure_frame unlinks itself from the closure and relinks the preceding closure_frame prior to this instance.
The local variables in the closure 'clos' above is default constructed in the stack inside function 'foo'. Once 'foo' is exited, all of these local variables are destructed. In some cases, default construction is not desirable and we need to initialize the local closure variables with some values. This can be done by passing in the initializers in a compatible tuple. A compatible tuple is one with the same number of elements as the destination and where each element from the destination can be constructed from each corresponding element in the source. Example:
tuple<int, char const*, int> init(123, "Hello", 1000);
closure_frame<my_closure> frame(clos, init);
Here now, our closure_frame's variables are initialized with int: 123, char const*: "Hello" and int: 1000.
[page:1 Lazy Construction and Conversions]
[h2 Lazy C++ Casts]
The set of lazy C++ cast template classes and functions provide a way of lazily casting certain type to another during parsing. The lazy C++ templates are (syntactically) used very much like the well known C++ casts:
A *a = static_cast_<A *>(_a_lambda_expression_);
These casts parallel the ones in the C++ language. Take note however that the ['lazy] versions have a trailing underscore.
*static_cast_<T>(lambda_expression)
*dynamic_cast_<T>(lambda_expression)
*const_cast_<T>(lambda_expression)
*reinterpret_cast_<T>(lambda_expression)
[blurb __note__ [*Acknowledgement:][br][br][*Hartmut Kaiser] implemented the lazy casts and constructors based on his original work on Spirit SE "semantic expressions" (the precursor of Phoenix).]
[h2 Lazy object construction]
A set of lazy constructor template classes and functions provide a way of lazily constructing an object of a type from an arbitrary set of lazy arguments in the form of lambda expressions. The construct_ templates are (syntactically) used very much like the well known C++ casts:
A a = construct_<A>(lambda_arg1, lambda_arg2, ..., lambda_argN);
where the given parameters are become the parameters to the contructor of the object of type A. (This implies, that type A is expected to have a constructor with a corresponsing set of parameter types.)
[blurb __tip__ The ultimate maximum number of actual parameters is limited by the preprocessor constant PHOENIX_CONSTRUCT_LIMIT. Note though, that this limit should not be greater than PHOENIX_LIMIT.]
[page Efficiency]
Now this is important. Operators that form expressions and statements, while truly expressive, should be used judiciously and sparingly. While aggressive compiler optimizations and inline code helps a lot to produce tighter and faster code, lazy operators and statements will always have more overhead compared to lazy- functions and bound simple functors especially when the logic gets to be quite complex. It is not only run-time code that hits a penalty, complex expressions involving lazy-operators and lazy- functions are also much more difficult to parse and compile by the host C++ compiler and results in much longer compile times.
[blurb __tip__ [*Lambda vs. Offline Functions][br][br]The best way to use the framework is to write generic off-line lazy functions (see functions) then call these functions lazily using straight-forward inline lazy-operators and lazy-statements.]
While it is indeed satisfying to impress others with quite esoteric uses of operator overloading and generative programming as can be done by lazy-operators and lazy-statements, these tools are meant to be used for the right job. That said, caveat-emptor.
[blurb __note__ need benchmarks, benchmarks, and more benchmarks]
[page Inside Phoenix]
This chapter explains in more detail how the framework operates. The information henceforth should not be necessary to those who are interested in just using the framework. However, a microscopic view might prove to be beneficial to more advanced programmers. But then again, it is really hard to classify what it means to be an "advanced programmer". Is knowledge of the C++ language as a language lawyer a prerequisite? Perhaps, but also perhaps not. As always, the information presented will always assume a friendly tone. Perhaps the prerequisite here is that the reader should have an "advanced imagination" :-) and a decent knowledge of C++ language rules.
[page:1 Tuples]
Tuples are the most basic infrastructure that the framework builds with. This sub-library provides a mechanism to bundle objects of arbitrary types in a single structure. Tuples hold heterogeneous types up to a predefined maximum.
Only the most basic functionality needed are provided. This is a straight-forward and extremely lean and mean library. Unlike other recursive list-like tuple implementations, this tuple library implementation uses simple structs similar to std::pair with specialization for 0 to N tuple elements, where N is a predefined constant. There are only 4 tuple operations to learn:
1) Construction
Here are examples on how to construct tuples:
typedef tuple<int, char> t1_t;
typedef tuple<int, std::string, double> t2_t;
// this tuple has an int and char members
t1_t t1(3, 'c');
// this tuple has an int, std::string and double members
t2_t t2(3, "hello", 3.14);
2) Member access
A member in a tuple can be accessed using the tuple's [] operator by specifying the Nth tuple_index. Here are some examples:
tuple_index<0> ix0; // 0th index == 1st item
tuple_index<1> ix1; // 1st index == 2nd item
tuple_index<2> ix2; // 2nd index == 3rd item
// Note zero based indexing. 0 = 1st item, 1 = 2nd item
t1[ix0] = 33; // sets the int member of the tuple t1
t2[ix2] = 6e6; // sets the double member of the tuple t2
t1[ix1] = 'a'; // sets the char member of the tuple t1
Access to out of bound indexes returns a nil_t value.
3) Member type inquiry
The type of an individual member can be queried. Example:
tuple_element<1, t2_t>::type
Refers to the type of the second member (again note zero based indexing, hence 0 = 1st item, 1 = 2nd item) of the tuple.
Access to out of bound indexes returns a nil_t type.
4) Tuple length
The number of elements in a tuple can be queried. Example:
int n = t1.length;
gets the number of elements in tuple t1.
length is a static constant. Thus, TupleT::length also works. Example:
int n = t1_t::length;
[page:1 Actors revisited]
This class is a protocol class for all actors. This class is essentially an interface contract. The actor class does not really know how how to act on anything but instead relies on the template parameter BaseT (from which the actor will derive from) to do the actual action. The template class actor is declared as:
template <typename BaseT>
struct actor : public BaseT {
actor();
actor(BaseT const& base);
/*...member functions...*/
};
[blurb __detail__ [*Curiously Recurring Template Pattern Inverse][br][br]Notice that actor derives from its template argument BaseT. This is the inverse of the curiously recurring template pattern (CRTP). With the CRTP, the actor is an abstract class with a DerivedT template parameter that is assumed to be its parametric subclass. This pattern however, "parametric base class pattern" (PBCP) for lack of a name, inverses the inheritance and makes actor a concrete class. Anyway, be it CRTP or PBCP, actor is a protocol class and either BaseT or DerivedT will have to conform to its protocol. Both CRTP and PBCP techniques has its pros and cons, of which is outside the scope of this document. CRTP should really be renamed "parametric subclass pattern (PSCP), but again, that's another story.]
An actor is a functor that is capable of accepting arguments up to a predefined maximum. It is up to the base class to do the actual processing or possibly to limit the arity (no. of arguments) passed in. Upon invocation of the functor through a supplied operator(), the actor funnels the arguments passed in by the client into a tuple and calls the base class' eval member function.
Schematically:
arg0 ---------|
arg1 ---------|
arg3 ---------|---> tupled_args ---> base.eval
... |
argN ---------|
actor::operator()(arg0, arg1... argN)
---> BaseT::eval(tupled_args);
Actor base classes from which this class inherits from are expected to have a corresponding member function eval compatible with the conceptual Interface:
template <typename TupleT>
actor_return_type
eval(TupleT const& args) const;
where args are the actual arguments passed in by the client funneled into a tuple (see tuple for details).
The actor_return_type can be anything. Base classes are free to return any type, even argument dependent types (types that are deduced from the types of the arguments). After evaluating the parameters and doing some computations or actions, the eval member function concludes by returning something back to the client. To do this, the forwarding function (the actor's operator()) needs to know the return type of the eval member function that it is calling. For this purpose, actor base classes are required t
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -