Boost logo

Boost :

From: Howard Hinnant (hinnant_at_[hidden])
Date: 1999-11-30 20:07:05


Ever since the empty_member thread started, I've been frustrated that I
can't optimize function objects with this technique because they might be
ordinary function pointers, and you can't derive from a function pointer.

Here's a concrete example: A SortedVec that takes both an allocator, and
a comparison object to sort by:

#include <memory>
#include <functional>

template <class T, class Compare = std::less<T>, class Allocator =
std::allocator<T> >
class SortedVec
{
public:
        typedef unsigned long size_type;
        SortedVec(const Compare& comp = Compare(), const Allocator& a =
Allocator())
                : comp_(comp, 0), alloc_(a, 0) {}
private:
        T* data_;
        empty_member<Compare, size_type> comp_; // member = size
                size_type& sz() {return comp_.member;}
                size_type sz() const {return comp_.member;}
        empty_member<Allocator, size_type> alloc_; // member = capacity
                size_type& cap() {return alloc_.member;}
                size_type cap() const {return alloc_.member;}
};

Here I've wishfully optimized the Compare object with empty_member. 99%
of the time this will work well. Most people if they don't use
std::less, will use another function object. But someone will try:

bool
comp(int x, int y)
{
        return x > y;
}

typedef bool (*Comp)(int, int);

#include <iostream>

int main()
{
        SortedVec<int> a;
        SortedVec<int, Comp> b(comp);
        std::cout << sizeof(a) << '\n' << sizeof(b) << '\n';
}

The SortedVec<int, Comp> won't compile because of the empty member
optimization. Really irritating!

How about specializing empty_member so that the first member can be
either a class or a pod? We would need to use a type_traits class (or
something) to determine if the first member is a pod or not. For
purposes of discussion, I've lumped this into call_traits because it was
handy, but that's not where I think it should go.

Might look something like:

    template<typename PossiblyEmptyBase, typename NonEmptyMember,
         bool FirstPod = call_traits<T1>::is_pod>
    struct empty_member;

    template<typename PossiblyEmptyBase, typename NonEmptyMember>
    struct empty_member< PossiblyEmptyBase, NonEmptyMember, true >
       : public PossiblyEmptyBase
    {
        typedef PossiblyEmptyBase base_type;
        typedef NonEmptyMember member_type;

        NonEmptyMember member;
        ...
    };

    template<typename PossiblyEmptyBase, typename NonEmptyMember>
    struct empty_member< PossiblyEmptyBase, NonEmptyMember, false >
       : public PossiblyEmptyBase
    {
        typedef PossiblyEmptyBase base_type;
        typedef NonEmptyMember member_type;

        PossiblyEmptyBase pem;
        NonEmptyMember member;
        ...
    };

Heck, while we're at it, why not make both first and second members
possibly zero-sized. This might look a little more like a souped
up/specialized std::pair. Here's a complete working example:

------------cut here------------

template< typename T > struct call_traits
{
    typedef const T & type;
    static const bool is_pod = false;
    enum { by_value = false };
};

template< typename T >
struct call_traits< T* >
{
        typedef T * const type;
    static const bool is_pod = true;
        enum { by_value = true };
};

template< typename T >
struct call_traits< T& >
{
        typedef T & type;
    static const bool is_pod = true;
        enum { by_value = false };
};

template< typename T, int sz >
struct call_traits< T[sz] >
{
        typedef T * const type;
    static const bool is_pod = true;
        enum { by_value = true };
};

template<>
struct call_traits< int >
{
        typedef int const type;
    static const bool is_pod = true;
        enum { by_value = true };
};

template<>
struct call_traits< unsigned long >
{
        typedef unsigned long const type;
    static const bool is_pod = true;
        enum { by_value = true };
};

template<typename T1, typename T2,
        bool FirstPod = call_traits<T1>::is_pod,
        bool SecondPod = call_traits<T2>::is_pod> struct empty_member;

template<typename T1, typename T2>
struct empty_member<T1, T2, false, false>
        : private T1
{
        typedef T1 first_type;
        typedef T2 second_type;

    empty_member() {}

        empty_member( typename call_traits<first_type>::type x,
                      typename call_traits<second_type>::type y )
                : first_type(x), second_(y) {}

        // To avoid ambiguity, do not use the single argument constructors
        // if there is any possibility PossiblyEmptyBase and NonEmptyMember
        // could be the same class.
        explicit empty_member( typename call_traits<first_type>::type x )
                : first_type(x) {}

        explicit empty_member( typename call_traits<second_type>::type y )
                : second_(y) {}

        first_type& first() { return *this; }
        const first_type& first() const { return *this; }

        second_type& second() { return second_; }
        const second_type& second() const { return second_; }

private:
        second_type second_;
};

template<typename T1, typename T2>
struct empty_member<T1, T2, false, true>
        : private T1
{
        typedef T1 first_type;
        typedef T2 second_type;

    empty_member() {}

        empty_member( typename call_traits<first_type>::type x,
                      typename call_traits<second_type>::type y )
                : first_type(x), second_(y) {}

        // To avoid ambiguity, do not use the single argument constructors
        // if there is any possibility PossiblyEmptyBase and NonEmptyMember
        // could be the same class.
        explicit empty_member( typename call_traits<first_type>::type x )
                : first_type(x) {}

        explicit empty_member( typename call_traits<second_type>::type y )
                : second_(y) {}

        first_type& first() { return *this; }
        const first_type& first() const { return *this; }

        second_type& second() { return second_; }
        const second_type& second() const { return second_; }

private:
        second_type second_;
};

template<typename T1, typename T2>
struct empty_member<T1, T2, true, false>
        : private T2
{
        typedef T1 first_type;
        typedef T2 second_type;

    empty_member() {}

        empty_member( typename call_traits<first_type>::type x,
                      typename call_traits<second_type>::type y )
                : first_(x), second_type(y) {}

        // To avoid ambiguity, do not use the single argument constructors
        // if there is any possibility PossiblyEmptyBase and NonEmptyMember
        // could be the same class.
        explicit empty_member( typename call_traits<first_type>::type x )
                : first_(x) {}

        explicit empty_member( typename call_traits<second_type>::type y )
                : second_type(y) {}

        first_type& first() { return first_; }
        const first_type& first() const { return first_; }

        second_type& second() { return *this; }
        const second_type& second() const { return *this; }

private:
        first_type first_;
};

template<typename T1, typename T2>
struct empty_member<T1, T2, true, true>
{
        typedef T1 first_type;
        typedef T2 second_type;

    empty_member() {}

        empty_member( typename call_traits<first_type>::type x,
                      typename call_traits<second_type>::type y )
                : first_(x), second_(y) {}

        // To avoid ambiguity, do not use the single argument constructors
        // if there is any possibility PossiblyEmptyBase and NonEmptyMember
        // could be the same class.
        explicit empty_member( typename call_traits<first_type>::type x )
                : first_(x) {}

        explicit empty_member( typename call_traits<second_type>::type y )
                : second_(y) {}

        first_type& first() { return first_; }
        const first_type& first() const { return first_; }

        second_type& second() { return second_; }
        const second_type& second() const { return second_; }

private:
        first_type first_;
        second_type second_;
};

#include <memory>
#include <functional>

template <class T, class Compare = std::less<T>, class Allocator =
std::allocator<T> >
class SortedVec
{
public:
        typedef unsigned long size_type;
        SortedVec(const Compare& comp = Compare(), const Allocator& a =
Allocator())
                : comp_(comp, 0), alloc_(a, 0) {}

        size_type size() const {return sz();}
private:
        T* data_;
        empty_member<Compare, size_type> comp_; // member = size
                Compare& Comp() {return comp_.first();}
                const Compare& Comp() const {return comp_.first();}
                size_type& sz() {return comp_.second();}
                size_type sz() const {return comp_.second();}
        empty_member<Allocator, size_type> alloc_; // member = capacity
                Allocator& Alloc() {return alloc_.first();}
                const Allocator& Alloc() const {return alloc_.first();}
                size_type& cap() {return alloc_.second();}
                size_type cap() const {return alloc_.second();}
};

bool
comp(int x, int y)
{
        return x > y;
}

typedef bool (*Comp)(int, int);

struct A {};
struct B {};

#include <iostream>

int main()
{
        SortedVec<int, Comp> a(comp);
        SortedVec<int> b;
        std::cout << "sizeof(a) = " << sizeof(a) << "\nsizeof(b) = " <<
sizeof(b) << '\n';
        std::cout << "a.size() = " << a.size() << '\n';
        empty_member<A, B> c;
        std::cout << "sizeof(c) = " << sizeof(c) << '\n';
}

------------finish cut here------------

This prints out (on my system):

sizeof(a) = 16
sizeof(b) = 12
a.size() = 0
sizeof(c) = 1

I haven't double checked everything, so there may be bugs above. And
there may be better ways of implementing parts of it. But I think the
basic idea would be a valuable addition to empty_member.

-Howard


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk