Printing argv with forrange loop

Viewed 275
int main(int argc, const char** argv) {

    std::cout << "Hello" << std::endl;

    char arr2d[][4] = {"ABC", "DEF"};

    for (char *i : arr2d)
    {
        std::cout << i << std::endl;
    }

In here, I evaluate the job of forrange as this: "For each character array in arr2d, print it it to console". And this works, so, my understanding, at least, should be correct. The output to the above code snippet is,

muyustan@mint:~/Desktop/C_Files/oop$ g++ main.cpp -o main && ./main
Hello
ABC
DEF

as expected.

However, if I try this one,

int main(int argc, const char** argv) {

    std::cout << "Hello" << std::endl;

    char arr2d[][4] = {"ABC", "DEF"};

    for (const char *i : argv)
    {
        std::cout << i << std::endl;
    }

First the IDE warns me with,

this range-based 'for' statement requires a suitable "begin" function and none was found

And if I try to compile, I get:

muyustan@mint:~/Desktop/C_Files/oop$ g++ main.cpp -o main && ./main
main.cpp: In function ‘int main(int, const char**)’:
main.cpp:30:26: error: ‘begin’ was not declared in this scope
     for (const char *i : argv)
                          ^~~~
main.cpp:30:26: note: suggested alternative:
In file included from /usr/include/c++/7/string:51:0,
                 from /usr/include/c++/7/bits/locale_classes.h:40,
                 from /usr/include/c++/7/bits/ios_base.h:41,
                 from /usr/include/c++/7/ios:42,
                 from /usr/include/c++/7/ostream:38,
                 from /usr/include/c++/7/iostream:39,
                 from main.cpp:1:
/usr/include/c++/7/bits/range_access.h:105:37: note:   ‘std::begin’
   template<typename _Tp> const _Tp* begin(const valarray<_Tp>&);
                                     ^~~~~
main.cpp:30:26: error: ‘end’ was not declared in this scope
     for (const char *i : argv)
                          ^~~~
main.cpp:30:26: note: suggested alternative:
In file included from /usr/include/c++/7/string:51:0,
                 from /usr/include/c++/7/bits/locale_classes.h:40,
                 from /usr/include/c++/7/bits/ios_base.h:41,
                 from /usr/include/c++/7/ios:42,
                 from /usr/include/c++/7/ostream:38,
                 from /usr/include/c++/7/iostream:39,
                 from main.cpp:1:
/usr/include/c++/7/bits/range_access.h:107:37: note:   ‘std::end’
   template<typename _Tp> const _Tp* end(const valarray<_Tp>&);

So, why argv behaves differently than my arr2d[][4] ? Aren't both of them pointers of char pointers(char arrays or strings(?)) ?

And if something wrong with my understanding, what should be the structre of printing ingreditens of argv with a forrange?

5 Answers

this is something I have been told wrong then, when I was dealing with C back then, at somewhere I have read that "arrays are also pointers!".

There are couple of finer points one must understand regarding that statement.

  1. Arrays decay to pointers in most contexts and but arrays are still different than pointers.

    • When used as argument to sizeof, the following two will result in different answers.

      char const* ptr = "Some text.";
      char array[] = "some text.";
      
      std::cout << sizeof(ptr) << std::endl;    // prints sizeof the pointer.
      std::cout << sizeof(array) << std::endl;  // prints sizeof the array.
      
    • When used as an argument to the addressof operator.

      char const* ptr1 = "Some text.";
      char array[] = "some text.";
      
      char const** ptr2 = &ptr1;      // OK.
      char** ptr3 = &array;           // Error. Type mismatch.
      char (*ptr4}[11] = &array;      // OK.
      
  2. 2D arrays can decay to pointers to 1D arrays but they don't decay to pointers to pointers.

     int array1[10];
     int* ptr1 = array1;          // OK. Array decays to a pointer
    
    
     int array2[10][20];
     int (*ptr2)[20] = array2;    // OK. 2D array decays to a pointer to 1D array.
     int**  ptr3 = array2;        // Error. 2D array does not decay to a pointer to a pointer.
    

If you want to apply a range-based for to argv, it's probably easiest if you start by creating a vector containing the arguments:

#include <iostream>
#include <vector>

int main(int argc, char **argv){ 
    std::vector<std::string> args(argv, argv+argc);

    for (auto const &arg : args) {
        std::cout << arg << "\n"; // don't use `endl`.
    }
}

As far as argv compared to a 2D array, the difference is fairly simple. When you have an array declaration, the array can be passed by reference to a template, which can figure out its size:

template <class T, size_t N>
size_t array_size(T (&array)[N]) {
    return N;
}

int foo[2][3];
std::cout << array_size(foo) << "\n";

char bar[12][13][14];
std::cout << array_size(bar) << "\n";

...but, argv doesn't have a statically visible definition like that, from which the compiler would be able to deduce its size. In a typical case, there's code that runs outside main that inspects the command line, and allocates it dynamically instead.

The range-for expression works with iterators (pointers are a type of iterator), and it requires an iterator to the beginning and end of the range. It obtains those by passing the range to std::begin and std::end.

The type of arr2d is char[2][4]. As an array, it carries information about its size as part of its type. There are template overloads for std::begin and std::end that accept a reference to an array and return a pointer to its first and one-past-the-last element, respectively.

The type of argv is char**. It's just a pointer to a pointer to a char. The compiler has no idea either of those pointers point to the first element of an array, and these pointers carry no information about the length of the array that they point to. As such, there are no overloads of std::begin and std::end that accept a pointer since there's no way for std::end to figure out where the end of the array is in relation to the beginning from a pointer alone.

To use pointers with a range-for, you must provide the information about the array's length. In this case, you could construct a simple view over the array, since you know its length from argc:

template <typename T>
class PointerRange
{
private:
    T* ptr_;
    std::size_t length_;

public:
    PointerRange(T* ptr, std::size_t length)
        : ptr_{ptr},
          length_{length}
    {
    }

    T* begin() const { return ptr_; }
    T* end() const { return ptr_ + length_; }
};

int main(int argc, char** argv)
{
    for (char* arg : PointerRange(argv, argc)) {
        std::cout << arg << "\n";
    }
}

Live Demo

Once C++20 becomes available, std::span can take the place of the PointerRange defined above:

int main(int argc, char** argv)
{
    for (std::string_view arg : std::span{argv, argc}) {
        std::cout << arg << "\n";
    }
}

They behave differently because they have different types. This is confusing for beginners, but:

char **

is a pointer to pointer to char. In fact, in the case of argv, it is pointing to a sequence of pointers, each pointing to a nul-terminated string (which are sequences of characters).

The problem iterating those is that the size of those sequences is not known. The compilers cannot know argc is related to the first sequence mentioned above.

However:

char arr2d[][4] = {"ABC", "DEF"};

resolves to the type:

char [2][4]

Which is an array of arrays of char. In this case, the size is known (2), so you can iterate over it.

Finally, the compiler complains about std::begin because the range-based for loop is transformed into different equivalent code which uses std::begin etc. to do the iteration.

If your compiler has the span header (not a lot of them do right now) I imagine this would work.

// Example program
#include <iostream>
#include <string_view>
#include <span>

int main(int argc, char **argv)
{
  for (std::string_view s : std::span{argv, argc})
  {
      std::cout << s << std::endl;
  }
  return 0;
}

The only overhead with my example is having string_view find the null terminal. I tried it goldbolt.org but it seems that none of the compilers can find the span header. So take my advice lightly.

Related