13

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?

SwiftMango
  • 15,092
  • 13
  • 71
  • 136
  • 14
    `decltype` yields `int(&)[2]`, whilst plain `auto` forces a pointer conversion (same rules as template argument deduction). Just use `auto&`. – Xeo Feb 17 '14 at 08:29
  • 2
    @Xeo That should be an answer. – Angew is no longer proud of SO Feb 17 '14 at 08:33
  • @Xeo Can you make an answer? Also can you point me a URL that explain this deduction rule? – SwiftMango Feb 17 '14 at 08:35
  • @texasbruce The rules are defined in **§7.1.6.4 auto specifier** paragraph 6 and 7. You may find this useful: Scott Meyers' blog: [Universal References in C++11—Scott Meyers](http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers) – CouchDeveloper Feb 17 '14 at 08:55
  • 2
    please use `i != std::end(data)` instead of `<`: the latter assumes random access iterators, so passing a `std::list` to your `print2d` will fail. – TemplateRex Feb 17 '14 at 09:17
  • @couchdeveloper I do not see anywhere it says auto forces pointer conversion? – SwiftMango Feb 17 '14 at 09:28
  • @texasbruce "... the type of the declared variable using the declarator-id is determined from the type of its initializer using the rules for **template argument deduction**" - and in the meantime kindly explained by Marc Garcia. :) – CouchDeveloper Feb 17 '14 at 09:56
  • @couchdeveloper and where in standard says template argument deduction of array converts to pointer? – SwiftMango Feb 17 '14 at 10:01
  • @texasbruce 14.8.2.1 Deducing template arguments from a function call, under 2 "If P is not a reference type": "If A is an array type, the pointer type produced by the array-to-pointer standard conversion (4.2) is used in place of A for type deduction;" (where A represents the argument, and P the parameter of that function call) – CouchDeveloper Feb 17 '14 at 10:11
  • @texasbruce and: "4.2 Array-to-pointer conversion [conv.array] 1 An lvalue or rvalue of type “array of N T” or “array of unknown bound of T” can be converted to a prvalue of type “pointer to T”. The result is a pointer to the first element of the array." – CouchDeveloper Feb 17 '14 at 10:13
  • Have you tried a recent compiler? – PlasmaHH Feb 17 '14 at 11:05

1 Answers1

15

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)

Community
  • 1
  • 1
Mark Garcia
  • 17,424
  • 4
  • 58
  • 94
  • 2
    `const auto&` would be even better. – Jon Purdy Feb 17 '14 at 09:44
  • still, can you point me where in C++11 spec that says auto forces a pointer conversion? – SwiftMango Feb 17 '14 at 09:45
  • @JonPurdy `auto&` is enough, for it also deduces `const` if the source is also `const`, but yeah, that is much better if you don't intend to modify your variable. – Mark Garcia Feb 17 '14 at 09:45
  • I wrote this function myself so I dont really need explanation on how it works. The only explanation I need is what you described as `some template magic`. Xeo has been clear enough on why, and all I need other than that is an official reference. – SwiftMango Feb 17 '14 at 09:59
  • @texasbruce Sorry, I misread your comment. The part of the standard can be seen on `14.8.2 Template argument deduction`, specifically 14.8.2.1 onwards. – Mark Garcia Feb 17 '14 at 10:02
  • In C++1y i believe ‘decltype(auto)‘ will do the right thing too. Not experimented yet though. – John5342 Feb 17 '14 at 14:37
  • 1
    Nice answer. But why is `decltype(i)` not `const int (*) [2]` instead of `int (*) [2]`? After all, `data` was deduced as `const int (&) [2][2]` so `std::begin` should better return something pointing to const data? – Johannes Schaub - litb Feb 18 '14 at 13:26