SwiftMango
SwiftMango

Reputation: 15304

Why decltype works here but not auto?

I have the code as below:

template <typename T, typename sepT = char>
void print2d(const T &data, sepT sep = ',') {
    for(auto i = std::begin(data); i < std::end(data); ++i) {
        decltype(*i) tmp = *i;
        for(auto j = std::begin(tmp); j < std::end(tmp); ++j) {
            std::cout << *j << sep;
        }
        std::cout << std::endl;
    }
}

int main(){
    std::vector<std::vector<int> > v = {{11}, {2,3}, {33,44,55}};
    print2d(v);

    int arr[2][2] = {{1,2},{3,4}};
    print2d(arr);

    return 0;
}

If I change the decltype to auto, it won't compile and complain (partial error):

2d_iterator.cpp: In instantiation of ‘void print2d(const T&, sepT) [with T = int [2][2]; sepT = char]’:
2d_iterator.cpp:21:21:   required from here
2d_iterator.cpp:9:36: error: no matching function for call to ‘begin(const int*&)’
2d_iterator.cpp:9:36: note: candidates are:
In file included from /usr/lib/gcc/x86_64-redhat-linux/4.7.2/../../../../include/c++/4.7.2/string:53:0,
                 from /usr/lib/gcc/x86_64-redhat-linux/4.7.2/../../../../include/c++/4.7.2/bits/locale_classes.h:42,
                 from /usr/lib/gcc/x86_64-redhat-linux/4.7.2/../../../../include/c++/4.7.2/bits/ios_base.h:43,
                 from /usr/lib/gcc/x86_64-redhat-linux/4.7.2/../../../../include/c++/4.7.2/ios:43,
                 from /usr/lib/gcc/x86_64-redhat-linux/4.7.2/../../../../include/c++/4.7.2/ostream:40,
                 from /usr/lib/gcc/x86_64-redhat-linux/4.7.2/../../../../include/c++/4.7.2/iterator:64,

Why is this happening?

Upvotes: 13

Views: 571

Answers (1)

Mark Garcia
Mark Garcia

Reputation: 17708

The answer summed-up in one comment:

decltype yields int(&)[2], whilst plain auto forces a pointer conversion (same rules as template argument deduction). Just use auto&. - Xeo


@Xeo's comment-answer basically says that because auto involves the same rules as template argument type deduction, auto deduces a pointer (int*) type out of the source's array type (of i, specifically int(&)[2]).

There is something great in your code: it actually demonstrates how template type deduction behaves when the parameter is a reference and how the reference affects how the type is being deduced.

template <typename T, typename sepT = char>
void print2d(const T &data, sepT sep = ',') {
    ...
}

...

int arr[2][2] = {{1,2},{3,4}};
print2d(arr);

You can see that data is of type const T&, a reference to a const T. Now, it is being passed with arr, whose type is int[2][2], which is an array of two arrays of two ints (whoo!). Now come template argument type deduction. On this situation, it rules that with data being a reference, T should be deduced with the original type of the argument, which is int[2][2]. Then, it applies any qualifications to the parameter type to the parameter, and with data's qualified type being const T&, the const and & qualifiers are applied and so data's type is const int (&) [2][2].

template <typename T, typename sepT = char>
void print2d(const T &data, sepT sep = ',') {
    static_assert(std::is_same<T, int[2][2]>::value, "Fail");
    static_assert(std::is_same<decltype(data), const int(&)[2][2]>::value, "Fail");
}

...

int arr[2][2] = {{1,2},{3,4}};
print2d(arr);

LIVE CODE

However, if data would have been a non-reference, template argument type deduction rules that if the argument's type is an array type (e.g. int[2][2]), the array type shall "decay" to its corresponding pointer type, thus making int[2][2] into int(*)[2] (plus const if parameter is const) (fix courtesy of @Xeo).


Great! I just explained the part that is entirely not what caused the error. (And I just explained a great deal of template magic)...

... Nevermind about that. Now to the error. But before we go, keep this on your mind:

auto == template argument type deduction
         + std::initializer_list deduction for brace init-lists   // <-- This std::initializer_list thingy is not relevant to your problem,
                                                                  //    and is only included to prevent any outbreak of pedantry.

Now, your code:

for(auto i = std::begin(data); i < std::end(data); ++i) {
    decltype(*i) tmp = *i;
    for(auto j = std::begin(tmp); j < std::end(tmp); ++j) {
        std::cout << *j << sep;
    }
    std::cout << std::endl;
}

Some prerequisites before the battle:

  • decltype(data) == const int (&) [2][2]
  • decltype(i) == const int (*) [2] (see std::begin), which is a pointer to an int[2].

Now when you do decltype(*i) tmp = *i;, decltype(*i) would return const int(&)[2] , a reference to an int[2] (remember the word dereference). Thus, it is also tmp's type. You preserved the original type by using decltype(*i).

However, when you do

auto tmp = *i;

Guess what decltype(tmp) is: int*! Why? Because all of the blabbery-blablablah above, and some template magic.

So, why the error with int*? Because std::begin expects an array type, not its lesser decayed-to pointer. Thus, auto j = std::begin(tmp) would cause an error when tmp is int*.

How to solve (also tl;dr)?

  • Keep as-is. Use decltype.

  • Guess what. Make your autoed variable a reference!

    auto& tmp = *i;
    

    LIVE CODE

    or

    const auto& tmp = *i;
    

    if you don't intend to modify the contents of tmp. (Greatness by Jon Purdy)


Moral of the story: A great comment saves a man a thousand words.


UPDATE: added const to the types given by decltype(i) and decltype(*i), as std::begin(data) would return a const pointer due to data also being const (fix by litb, thanks)

Upvotes: 15

Related Questions