2
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?

muyustan
  • 1,555
  • 1
  • 11
  • 23
  • 3
    No, `argv` is a pointer to a pointer to a `char`. `arr2d` is an array of arrays of `char`s. **Arrays are not pointers**. – Miles Budnek Apr 06 '20 at 16:08
  • @MilesBudnek 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!". – muyustan Apr 06 '20 at 16:12
  • You heard that most likely in relation to arrays decaying to pointers under certain circumstances (see eg https://stackoverflow.com/questions/1461432/what-is-array-decaying), but putting them as equivalent is just wrong and the source of many misunderstandings and bugs in code – 463035818_is_not_an_ai Apr 06 '20 at 16:14
  • How would you define an array of pointers then? For checking contents of argv using a ranged loop, check [this](https://stackoverflow.com/questions/8572991/how-to-write-the-range-based-for-loop-with-argv) –  Apr 06 '20 at 16:14
  • Note also that C is not C++. – mkrieger1 Apr 06 '20 at 16:14

5 Answers5

3

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.
    
R Sahu
  • 204,454
  • 14
  • 159
  • 270
2

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.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • I was about to ask for a more explanatory answer, I will examine your edited version now. I also shold say that it seems you forgot to add `#include `. – muyustan Apr 06 '20 at 16:19
  • @usr: It depends. If that's all you're doing with it, then probably yes. But if you're doing much with it, putting it into a vector can be thoroughly worthwhile. Of course, you can also go for the middle ground of a `std::vector args(argv, argv+argc)`. – Jerry Coffin Apr 06 '20 at 16:24
  • Or something in between, like a `std::vector`. – Ted Lyngmo Apr 06 '20 at 16:29
  • 3
    `for (std::string_view s : std::span{argv, argc})` in C++20 – Jarod42 Apr 06 '20 at 16:35
2

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";
    }
}
Miles Budnek
  • 28,216
  • 2
  • 35
  • 52
  • or even `for (std::string_view s : std::span{argv, argc})` in C++20. – Jarod42 Apr 06 '20 at 16:37
  • @Jarod42 Yep, just changed it. I can never keep the ranges stuff straight. – Miles Budnek Apr 06 '20 at 16:38
  • well, this answer is way above my level right now, however here what I understood roughly: for a forrange loop to work, the length(or start and end) of the iterable(I don't know the right word for this) must be defined. So, for a 2D array, it is known, however, for a `char **` it is not known. And this is the basis of the problem. Do I at least catch the right track? – muyustan Apr 06 '20 at 17:21
  • 1
    @muyustan Yeah, that's the right track. I edited to add a bit more info. Basically, an array has a known size, but a pointer can point to anything. You have to tell the range-for how long the array pointed to is for it to be able to work with a pointer, and you do that by wrapping the pointer in some sort of range class. – Miles Budnek Apr 06 '20 at 17:33
2

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.

Acorn
  • 24,970
  • 5
  • 40
  • 69
  • let me try to explain my understanding. So, you cannot know the end of a `char **`, is this where the problem arises from? But for case of a `char *` its ending point is known by default because it is null terminated(`\0`), right? – muyustan Apr 06 '20 at 17:23
  • @muyustan Kind of! For a `char *` the compiler doesn't know either, but it works because you are calling `std::cout << i`, which means to print the string starting there *assuming* it is a `\0`-terminated string (if it isn't because you got it wrong, then it is undefined behavior: the program will start printing garbage until it finds a `\0` or crash when accessing an invalid memory address). – Acorn Apr 06 '20 at 21:43
  • Insertion of `\0` to the end of a "string" happens at the background generally then, for example, when initiating a `char str[] = "hello"`, right? Because I have never inserted one myself xd – muyustan Apr 06 '20 at 21:52
  • @muyustan It depends on what you do, for the case you just wrote (a string literal), it is added. Some functions add it, too. Others do not. If you are writing a string to memory yourself, you will have to do it. That is why it is generally best that you use `std::string` instead, which handles things for you, unless you have specific requirements. – Acorn Apr 06 '20 at 22:20
1

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.