[PROTO] Custom functions, scalar terminal and make_expr vs details::as_expr_if
I have a large number of custom unary and bin ary function that I map
using unary_expr and binary_expr.
The problem is the following : my grammar has integral or floating point
scalar terminal.
So, when I want to create a binary function that takes any elements of
my domain, I'm boudn to do the following :
template
Joel Falcou wrote:
I have a large number of custom unary and bin ary function that I map using unary_expr and binary_expr. The problem is the following : my grammar has integral or floating point scalar terminal. So, when I want to create a binary function that takes any elements of my domain, I'm boudn to do the following :
template
typename result_of::make_expr<....>::type my_custom_binary_func( L const& l, R const& r ) { return make_expr( ... ); } Alas, as I already discussed with Eric, nothign prevent me to use my_custom_binary_func over types that aren't in my grammar.
The first question to ask is if it is really absolutely necessary to prevent users from creating invalid expressions with my_custom_binary_func(), or if you can catch the error someplace else; e.g., when users try to evaluate the expression. Reporting a hard error later can often yield a better error message then using SFINAE to prune overload sets earlier.
To do so, I was told to use SFINAE to prune unwanted element by matching the result type of make_expr on my grammar. However, this proves cumbersome and slow down compile time by *a large factor* when I have a large number of such function (and I have like 50 or 60).
So i wandered into proto source code and looked at how proto solves this problem for its binary operator. This solution seems more elegant but as it live sin proto::detail, I don't think I'm intended to use it.
So, how can I have a simple and fast creation of custom expression ? Am I missing something blatant ?
If you really want to prevent users from creating invalid expressions
with these functions, you can do something like the following ...
template<typename T>
struct valid_terminal {}; // empty!
template<>
struct valid_terminal<int> { typedef int type; };
// other specializations for valid terminal types ...
// When L and R are both proto expressions
template
::type my_custom_binary_func( L const& l, R const& r ) { /*...*/ }
// When L is a proto expression and R is a terminal
template
::type my_custom_binary_func( L const& l, R const& r ) { /*...*/ }
// When R is a proto expression and L is a terminal
template
::type my_custom_binary_func( L const& l, R const& r ) { /*...*/ }
// When L and R are valid terminal types
template
::type my_custom_binary_func( L const& l, R const& r ) { /*...*/ }
If you want to improve compile times even more, you can avoid make_expr
entirely and code by-hand what make_expr would do. So, e.g., the second
overload above would be:
// When L is a proto expression and R is a terminal
template
my_custom_binary_func( L const& l, R const& r ) { /*...*/ } And in the body of my_custom_binary_func() you initialize an object of this type directly instead of calling make_expr(). Naturally, you'd put all this gunk in a macro so you can easily define all 50 or so of your custom binary functions. Note that I don't think this solution is particularly better than just letting my_custom_binary_func create invalid expressions and catching the mistake later. HTH, -- Eric Niebler BoostPro Computing http://www.boostpro.com
Eric Niebler a écrit :
The first question to ask is if it is really absolutely necessary to prevent users from creating invalid expressions with my_custom_binary_func(), or if you can catch the error someplace else; e.g., when users try to evaluate the expression. Reporting a hard error later can often yield a better error message then using SFINAE to prune overload sets earlier. OK then, but I don't really want my user face a large template error because they mismatched some binary function arguments when they can have a nice 'no such function F(A,B)' error message instead.
The solution of enumerating the (expr,expr) (expr,scalar) (scalar,expr) variants is maybe what's needed then ? The only problem I see is that enumerating the ternary one will be real long.
If you want to improve compile times even more, you can avoid make_expr entirely and code by-hand what make_expr would do. So, e.g., the second overload above would be: <snip> Why is this faster than calling make_expr ? Even worse, doesn't it defeat the proto purpose ? Or am I in a situation I have to write dirty code like this and only use Proto as a expression builder ?
-- ___________________________________________ Joel Falcou - Assistant Professor PARALL Team - LRI - Universite Paris Sud XI Tel : (+33)1 69 15 66 35
Joel Falcou wrote:
Eric Niebler a écrit :
The first question to ask is if it is really absolutely necessary to prevent users from creating invalid expressions with my_custom_binary_func(), or if you can catch the error someplace else; e.g., when users try to evaluate the expression. Reporting a hard error later can often yield a better error message then using SFINAE to prune overload sets earlier. OK then, but I don't really want my user face a large template error because they mismatched some binary function arguments when they can have a nice 'no such function F(A,B)' error message instead.
With SFINAE, the error your users are likely to see is something like "No such function f(A,B)" followed by an exhaustive list of potential matches. Then your users are left to figure out for themselves why none of those functions are a good match. In my experience, a message like "E doesn't match the grammar G" is simpler, often shorter, and has the benefit of taking your users to a single line in your code where you can stick a big comment describing *why* the compile-time assertion failed.
The solution of enumerating the (expr,expr) (expr,scalar) (scalar,expr) variants is maybe what's needed then ? The only problem I see is that enumerating the ternary one will be real long.
If you want to improve compile times even more, you can avoid make_expr entirely and code by-hand what make_expr would do. So, e.g., the second overload above would be: <snip> Why is this faster than calling make_expr ?
To figure it out, just count template instantiations. You're building a new expression type, so you can't avoid instantiating those types. But make_expr is itself a template, and you can avoid that cost.
Even worse, doesn't it defeat the proto purpose ?
What do you want from me?
Or am I in a situation I have to write dirty code like this and only use Proto as a expression builder ?
I gave you a few options to choose from. I told you which I prefer. Pick the one that meets your needs. If you absolutely need this to compile as fast as possible, do what it takes to bring down the number of templates you instantiate. That might mean eschewing some of the nice high-level facilities Proto provides. -- Eric Niebler BoostPro Computing http://www.boostpro.com
Eric Niebler a écrit :
With SFINAE, the error your users are likely to see is something like "No such function f(A,B)" followed by an exhaustive list of potential matches. Then your users are left to figure out for themselves why none of those functions are a good match.
In my experience, a message like "E doesn't match the grammar G" is simpler, often shorter, and has the benefit of taking your users to a single line in your code where you can stick a big comment describing *why* the compile-time assertion failed.
That's a very valid position. So basically, a good ol'MPL_ASSERT_MSG where it fits is better.
To figure it out, just count template instantiations. You're building a new expression type, so you can't avoid instantiating those types. But make_expr is itself a template, and you can avoid that cost.
Indeed, I just did the math and the comparison (went down from 9 to 3s)
There's one more thing I wanted to say about this. No, it doesn't defeat Proto's purpose. Proto provides high-level facilities so you can do a lot in just a little code. This is very important when you are just starting out with your DSEL and your code is in flux. Use those high-level facilities to keep your design fungible. When you're happy with your design, *then* you can go back and optimize for compile times by replacing some of the more expensive facilities with more efficient lower-level ones.
A good example of this is transforms. Joel is using composite transforms (object and callable transforms, specified with function types) when building Spirit2x. It makes the code very small and nimble, and he can easily change his mind and make radical changes at this stage. When things are stable, he'll go through and replace all the composite transforms with custom primitive transforms to improve compile times. Does that mean composite transforms are useless? Not at all. The fact is that I was running my own DSL for years now using hand made code and lots of those 'low level' structures. I first saw Proto as a 'end-all' solution. I obviously miss the largest benefit of Proto design which is indeed going down low level when needed. OK, things are sensibly clearer now. Using the low level constructor was something I thought was frowned upon. Guess I was wrong. I'll then wait for my design to stabilize, cope with compiel-time for a few and do as you said. Thanks you very much.
Thanks for this valuable insight. -- ___________________________________________ Joel Falcou - Assistant Professor PARALL Team - LRI - Universite Paris Sud XI Tel : (+33)1 69 15 66 35 -- ___________________________________________ Joel Falcou - Assistant Professor PARALL Team - LRI - Universite Paris Sud XI Tel : (+33)1 69 15 66 35
Joel Falcou wrote:
Eric Niebler a écrit :
If you want to improve compile times even more, you can avoid make_expr entirely and code by-hand what make_expr would do. So, e.g., the second overload above would be: <snip> Why is this faster than calling make_expr ? Even worse, doesn't it defeat the proto purpose ?
There's one more thing I wanted to say about this. No, it doesn't defeat Proto's purpose. Proto provides high-level facilities so you can do a lot in just a little code. This is very important when you are just starting out with your DSEL and your code is in flux. Use those high-level facilities to keep your design fungible. When you're happy with your design, *then* you can go back and optimize for compile times by replacing some of the more expensive facilities with more efficient lower-level ones. A good example of this is transforms. Joel is using composite transforms (object and callable transforms, specified with function types) when building Spirit2x. It makes the code very small and nimble, and he can easily change his mind and make radical changes at this stage. When things are stable, he'll go through and replace all the composite transforms with custom primitive transforms to improve compile times. Does that mean composite transforms are useless? Not at all. HTH, -- Eric Niebler BoostPro Computing http://www.boostpro.com
participants (2)
-
Eric Niebler
-
Joel Falcou