
Ion GaztaƱaga wrote:
Tobias Schwinger wrote:
Ion GaztaƱaga wrote: Also, please keep in mind that we are talking about a workaround, because decent compilers should factor out redundant machine code automatically.
Do you have any evidence of this? I've read somewhere that Visual 8 does something like this, but I hope this is not the COMDAT folding option. This redundant machine code erasing is performed by the linker, the code is still instantiated at compile time, so we are not going to save much compilation time.
No, it's not some new esoteric thing: Given some code like template<typename T> struct X { /* code inside that only uses T* */ } the compiler can determine that an incomplete type suffices for 'T' and that instances for any 'T' are interchangeable. Let' see: Last login: Fri Aug 10 16:48:12 on ttyp2 Welcome to Darwin! max:~ tosh$ cd ~/Lab/consulting/ max:~/Lab/consulting tosh$ cat code_bloat.cpp template< typename T > struct X { T* a; T* b; void swap() { T* tmp = a; a = b; b = tmp; } }; struct t1; #ifdef TRY_BLOAT struct t2; struct t3; struct t4; struct t5; struct t6; struct t7; struct t8; struct t9; #endif int main() { { X<t1> x; x.swap(); } #ifdef TRY_BLOAT { X<t2> x; x.swap(); } { X<t3> x; x.swap(); } { X<t4> x; x.swap(); } { X<t5> x; x.swap(); } { X<t6> x; x.swap(); } { X<t7> x; x.swap(); } { X<t8> x; x.swap(); } #else { X<t1> x; x.swap(); } { X<t1> x; x.swap(); } { X<t1> x; x.swap(); } { X<t1> x; x.swap(); } { X<t1> x; x.swap(); } { X<t1> x; x.swap(); } { X<t1> x; x.swap(); } #endif return 0; } max:~/Lab/consulting tosh$ g++ -O2 code_bloat.cpp ; ls -l a.out -rwxr-xr-x 1 tosh tosh 13928 Aug 10 19:26 a.out max:~/Lab/consulting tosh$ g++ -O2 code_bloat.cpp -DTRY_BLOAT ; ls -l a.out -rwxr-xr-x 1 tosh tosh 13928 Aug 10 19:26 a.out max:~/Lab/consulting tosh$ # here we go
In our example (list), all produced functions should be exactly the same, so the linker might have it easier (I don't know if shared libraries can also be optimized)
to erase redundant functions. However,
In terms of file size: Of course you can't throw out code that might not be present elsewhere ;-). In terms of memory footprint: Well, at least there are realtime OSes that support function level loading...
the two level architecture (node<->value ) would still be necessary, because functions are not exactly equal (the final transformation between the node and the hook is not equal, unlike the core algorithm).
Sure, we want to minimize the type dependencies of the node hook.
Second, the current usage is quite far away from "normally specifying template arguments", isn't it?
Well, for an average C++ programmer, a template parameters list
template <class ValueTraits, bool ConstantTimeSize, ...>
is surely easier to understand that seeing something like
template<class Policy1 = no_policy, class Policy2 = no_policy, >
and having to write
class_name <class T ,policy_name_a<parameter_to_policy2>, ,policy_name_a<parameter_to_policy3>...
specially when we only have 3 template parameters.
I want to understand the code without having to dig up the library header. The first argument to a container is usually the type, the other ones are >>gotta read into that library<<. For a set you can probably figure the second to be a compare function. For configuration options, however, positional arguments are a bad idea, IMO, because the resulting client code will not be self-explanatory. See http://www.boost-consulting.com/about/vision/expressiveness . Code is read more often than it is written. According to quite recent studies, 80% of software engineering efforts are -surprise surprise- maintainance.
I don't consider myself a novice, but I still don't easily grok this style (surely because no standard C++ utility uses this approach and I'm not used to it).
Anyway the derivation approach of your attached code should minimize instantiation problems. All the instantiated code is the same for all policy combinations, and only the front-end class is different.
Another question is if having different C++ types for otherwise semantically exact concepts (just that policies have been specified in another order) is a problem. Even if it's not a problem, that makes me really nervous.
Holding the implementation and using "dumb inline forwarders" (as suggested as the last resort method in the code sample) is documented to work in "C++ Templates" [Vandevoorde, Josuttis] as a technique to reduce code bloat.
That's why I think something like:
typedef options < void_pointer<void*> , constant_time_size<false> >::type my_options;
list<T, my_options>
is more straightforward and understandable, since the declaration of the class is something like:
template<class T, class Options = default_list_options> class list;
Any C++ programmer would instantly understand how to use the class ("Oh, a container of type T, where I can tweak some options"). I know, I'm from the old school, but you know: "One concept, one type" ;-)
<...>
Variant 2 can be very similar to variant 3 (we don't need an external options type and derive from default options). Just do the argument processing outside the class so that we don't complicate the class with argument processing:
typedef container < T , list_options < constant_time_size_operation<false> , void_pointer_type<offset_ptr<void> ,... // Unspecified options take default values > >::type > MyContainer;
where container is:
template<class T, class ListOptions = list_options<> > class container;
Container is exactly the same type for the same options and my nervousness disappears ;-)
That would be a great improvement of the interface. The reason why I suggested using a different approach is that 'list_options' is a non-trivial metaprogram which you said you wanted to avoid (see attached code). I'm still not convinced it is the better solution, but hey - it's your party ;-). Regards, Tobias #include <boost/mpl/vector.hpp> #include <boost/mpl/int.hpp> #include <boost/mpl/sort.hpp> #include <boost/mpl/fold.hpp> #include <boost/mpl/placeholders.hpp> #ifndef NDEBUG #include <boost/mpl/unique.hpp> #include <boost/mpl/assert.hpp> #include <boost/mpl/equal_to.hpp> #include <boost/mpl/size.hpp> #endif namespace mpl = boost::mpl; // Defaults namespace detail { struct container_xyz_defaults { static bool const constant_time_size = true; typedef void tag; template<typename T> struct value_traits { typedef T* pointer_type; typedef T& reference; // ... }; // ... }; } // namespace detail // Setters template<bool Enabled> struct constant_time_size { template<class Base> struct apply_options : Base { static bool const constant_time_size = Enabled; }; static int const option_id = 1; }; template<typename T> struct tag { template<class Base> struct apply_options : Base { typedef T tag; }; static int const option_id = 2; }; template<typename T> struct offset_ptr {}; // just pretend it's there... struct offset_ptr_storage { template<class Base> struct apply_options : Base { template<typename T> struct value_traits { typedef offset_ptr<T> pointer_type; typedef T& reference; // ... }; }; static int const option_id = 3; }; // ... namespace detail { struct option_id_less { template<class U, class V> struct apply { typedef bool value_type; typedef apply<U,V> type; static bool const value = U::option_id < V::option_id; }; }; struct option_id_equal { template<class U, class V> struct apply { typedef bool value_type; typedef apply<U,V> type; static bool const value = U::option_id == V::option_id; }; }; struct non_empty_option { template<class T> struct apply { typedef bool value_type; typedef apply<T> type; static bool const value = !! T::option_id; }; }; struct combine_options { template<class U, class V> struct apply { typedef typename V::template apply_options<U> type; }; }; } typedef mpl::na none_specified; template<class Option1 = none_specified, class Option2 = none_specified, class Option3 = none_specified, class Option4 = none_specified> class container_xyz_options { // set up sequence, sort typedef mpl::vector<Option1,Option2,Option3,Option4> options; typedef typename mpl::sort<options,detail::option_id_less>::type bases; #ifndef NDEBUG // check the options make sense typedef typename mpl::unique<bases, detail::option_id_equal>::type unique; typedef mpl::equal_to< mpl::size<bases>, mpl::size<unique> > unambiguous; BOOST_MPL_ASSERT_MSG(unambiguous::value, CONFLICTING_OPTIONS_SPECIFIED,(bases)); #endif // join options public: typedef typename mpl::fold<bases,detail::container_xyz_defaults, detail::combine_options>::type type; }; template<typename T, class Options = typename container_xyz_options<>::type > class container_xyz { public: typedef typename Options::template value_traits<T> value_traits; typedef typename value_traits::pointer_type pointer_type; typedef typename Options::tag tag; static bool const constant_time_size = Options::constant_time_size; // ... }; // Demonstrate it #include <iostream> #include <typeinfo> template<class C> void show_config(char const * name) { std::cout << name << ":" << std::endl << " tag: " << typeid( typename C::tag ).name() << std::endl << " pointer_type: " << typeid( typename C::pointer_type ).name() << std::endl << " constant_time_size: " << C::constant_time_size << std::endl << std::endl; } int main() { show_config< container_xyz< int, container_xyz_options< constant_time_size<false> >::type > > ("container_xyz< int, container_xyz_options< constant_time_size<false> >::type >"); show_config< container_xyz< int, container_xyz_options< offset_ptr_storage >::type > > ("container_xyz< int, container_xyz_options< offset_ptr_storage >::type >"); show_config< container_xyz< int, container_xyz_options< offset_ptr_storage, tag<int> >::type > > ("container_xyz< int, container_xyz_options< offset_ptr_storage, tag<int> >::type >"); return 0; }