Sunday, December 9, 2012

GCC 4.8 Has Automatic Return Type Deduction.

Note: GCC 4.8 is still in development; this article is based on Ubuntu's snapshot package of 4.8. I do not know about availability on other platforms. I say "has" because it does work and code can be written using it right now, even if it's in testing.  

Update: It turns out this feature has been implemented to test n3386. You can read the discussion and even see the patch on the mailing list: http://gcc.gnu.org/ml/gcc-patches/2012-03/msg01599.html

Are your two favourite C++11 features decltype and declval? I have mixed feelings. On one hand, it lets me write code like this

template< class X, class Y >
constexpr auto add( X&& x, Y&& y )
    -> decltype( std::declval<X>() + std::declval<Y>() )
{
    return std::forward<X>(x) + std::forward<Y>(y);
}

and know that it will work on any type and do the optimal thing if x or y should be moved or copied (like if X=std::string). On the other hand, it's tedious. "forward" and "declval" are both seven letter words that have to be typed out every time for every function, per variable. Then there's the std:: prefix and <X>(x) suffix. The only benefit of using declval over forward is a savings of one letter not typed.

But someone must have realized there's a better way. If the function is only one statement, and the return type is the declval of that statement, couldn't the compiler just figure out what I mean when I say this?

template< class X, class Y >
constexpr auto add( X&& x, Y&& y ) {
    return std::forward<X>(x) + std::forward<Y>(y);
}

March of this year, one Jason Merrill proposed just this (n3386) and GCC has already implemented it in 4.8 (change log), though it requires compiling with -std=c++1y. One can play with 4.8 on Ubuntu with gcc-snapshot. (Note that it doesn't modify your existing gcc install(s) and puts it in /usr/lib/gcc-snapshot/bin/g++. Also, I have been unable to install any but the third-to-most recent package.) I hope it is not too much trouble to install on other distros/platforms.

So if your favourite c++11 feature is decltype and declval, prepare to never use them again. The compiler can deduce the type for you implicitly, and better, and it works even if the function is longer than one line. Take for example, reasonably complex template functions like the liftM function I wrote for "Arrows and Keisli":

constexpr struct LiftM {
    template< class F, class M, class R = Return<typename std::decay<M>::type> >
    constexpr auto operator () ( F&& f, M&& m )
        -> decltype( std::declval<M>() >>= compose(R(),std::declval<F>()) )
    {
        return std::forward<M>(m) >>= compose( R(), std::forward<F>(f) );
    }

    template< class F, class A, class B >
    constexpr auto operator () ( F&& f, A&& a, B&& b )
        -> decltype( std::declval<A>() >>= compose (
                rcloset( LiftM(), std::declval<B>() ),
                closet(closet,std::declval<F>())
            ) )
    {
        return std::forward<A>(a) >>= compose (
            rcloset( LiftM(), std::forward<B>(b) ),
            closet(closet,std::forward<F>(f))
        );
    }
} liftM{};

Could be written instead:

constexpr struct LiftM {
    template< class F, class M >
    constexpr auto operator () ( F&& f, M&& m ) {
        using R = Return< typename std::decay<M>::type >;
        return std::forward<M>(m) >>= compose( R(), std::forward<F>(f) );
    }

    template< class F, class A, class B >
    constexpr auto operator () ( F&& f, A&& a, B&& b ) {
        return std::forward<A>(a) >>= compose (
            rcloset( LiftM(), std::forward<B>(b) ),
            closet(closet,std::forward<F>(f))
        );
    }
} liftM{};

Automatic type deduction didn't exactly make this function more obvious or simple, but it did remove the visual cruft and duplication of the definition. Now, if I improve this function to make it more clear, I won't have a decltype expression to have to also edit.

To be fair, this doesn't entirely replace decltype. auto doesn't perfect forward. But it seems to work as expected, most of the time.

For another example of the use-case of auto return type deduction, consider this program:

#include <tuple>

int main() {
    auto x = std::get<0>( std::tuple<>() );
}

This, small, simple, and obviously wrong program generates an error message 95 lines long. Why? GCC has to check make sure this isn't valid for the std::pair and std::array versions of get, and when it checks the tuple version, it has to instantiate std::tuple_element recursively to find the type of the element. It actually checks for the pair version first, so one has to search the message for the obviously correct version and figure out why it failed. A simple one-off bug in your program could cause a massive and difficult to parse error message. We can improve this simply.

#include <tuple>

template< unsigned int i, class T >
auto get( T&& t ) {
    using Tup = typename std::decay<T>::type;
    static_assert( i < std::tuple_size<Tup>::value,
                   "get: Index too high!" );
    return std::get<i>( std::forward<T>(t) );
}

int main() {
    int x = get<0>( std::tuple<>() );
}

How much does this shrink the error by? Actually, it grew to 112 lines, but right at the top is

auto.cpp: In instantiation of 'auto get(T&&) [with unsigned int i = 0u; T = std::tuple<>]':
auto.cpp:13:36:   required from here
auto.cpp:7:5: error: static assertion failed: get: Index too high!

The error message might be a little bigger, but it tells you right off the bat what the problem is, meaning one has less to parse.

Similar to this static_assert example, typedefs done as default template arguments can be moved to the function body in many cases.

template< class X, class Y = A<X>, class Z = B<Y> >
Z f( X x ) {
    Z z;
    ...
    return z;
}

can now be written

template< class X > // Simpler signature.
auto f( X x ) {
    using Y = A<X>; // Type instantiation logic
    using Z = B<Y>; // done in-function.
    Z z;
    ...
    return z;
}

Looking forward.

This release of GCC also implements inheriting constructors, alignas, and attribute syntax. It also may have introduced a few bugs; for example, my library, which compiles with 4.7, does not with 4.8, producing many undefined references.

The other features of this release might not be quite so compelling, but automatic type deduction alone is one powerful enough to change the way coding is done in C++--again. Heavily templated code will become a breeze to write and maintain as figuring out the return type is often the hardest part. I find it encouraging that this has been implemented so quickly. Of coarse, it's not standard, at least not yet.

On a final note, I wonder how this will interact with the static if. It would be nice, were the following well-formed:

template< class X >
auto f( X x ) {
    if( std::is_same<int,X>::value )
        return "Hi"; // Int? Return string.
    else
        return x / 2; // Other? halve.
}

6 comments:

  1. It looks like Perl...

    ReplyDelete
  2. Wow, you're taking C++ to levels I gave up on (by moving to a new language.) Bravo. Keep fighting the good fight.

    ReplyDelete
  3. If you think that looks like Perl, I doubt you have much experience with Perl.

    ReplyDelete
  4. Looks like you want D.

    ReplyDelete
  5. Replies
    1. Nothing pops out for me from the working draft standard, the proposal, or the mailing list post, that even hints at interaction with noexcept. I think it's unnecessary. Most likely, the function will be inlined the same way lambdas are, so calling it can be agnostic of exception safety.

      However, I am not as familiar with exception safety as other parts of the language, so I may not understand how they interact.

      Delete

Please feel free to post any comments, concerns, questions, thoughts and ideas. I am perhaps more interested in what you have to say than you are in what I do.