Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why decltype works here but not auto?

Tags:

c++

c++11

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?

like image 813
SwiftMango Avatar asked Feb 17 '14 08:02

SwiftMango


People also ask

What is the difference between auto and decltype?

'auto' lets you declare a variable with a particular type whereas decltype lets you extract the type from the variable so decltype is sort of an operator that evaluates the type of passed expression.

Where is decltype used?

Use auto and decltype to declare a template function whose return type depends on the types of its template arguments. Or, use auto and decltype to declare a template function that wraps a call to another function, and then returns the return type of the wrapped function.

What is a decltype in c++?

In the C++ programming language, decltype is a keyword used to query the type of an expression. Introduced in C++11, its primary intended use is in generic programming, where it is often difficult, or even impossible, to express types that depend on template parameters.

Why use auto c++?

The auto keyword is a simple way to declare a variable that has a complicated type. For example, you can use auto to declare a variable where the initialization expression involves templates, pointers to functions, or pointers to members.


1 Answers

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)

like image 69
Mark Garcia Avatar answered Oct 03 '22 22:10

Mark Garcia