Reputation: 15304
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
Reputation: 17708
The answer summed-up in one comment:
decltype
yieldsint(&)[2]
, whilst plainauto
forces a pointer conversion (same rules as template argument deduction). Just useauto&
. - 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 int
s (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);
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 auto
ed variable a reference!
auto& tmp = *i;
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