9
\$\begingroup\$

I was thinking about how to traverse an array without an int iterator, and I'm curious if my answer is good/bad.

The first for loop simply populates each element of the array.

The second for loop traverses through the array. The int pointer, curr, initially points to the first address of the array then goes on to momentarily hold the address of each element of the array. The int pointer, ptrLastElement, contains the address of the last element of the array arr.

The check condition is essentially the difference of the address contained in ptrLastElement and the address contained in curr. If the difference is less than 0, then we've ran out of elements to display.

My main questions are:

  1. Would this idea ever be useful?
  2. Is this a terrible idea?
  3. Did I do anything incredibly stupid?
  4. How can I improve this and/or make it more generic?

main.cpp

#include <iostream>

int main()
{
    const size_t SIZE = 10;
    int arr[SIZE];
    int * ptrLastElement = arr + (SIZE-1);
    
    // Populate the array, arr, using int iterator
    for (int i = 0; i < SIZE; ++i)
    {
        arr[i] = i;
    }
    
    // Traversing arr using a pointer
    for (int * curr = arr; (ptrLastElement - curr) >= 0 ; ++curr)
    {
        std::cout << curr << "\t" << *curr << std::endl;
    }
    
    return 0;
}
\$\endgroup\$
3
  • 1
    \$\begingroup\$ I'm not very knowledgeable with C++, but (ptrLastElement - curr) >= 0 looks like a less readable version of curr <= ptrLastElement \$\endgroup\$
    – phflack
    Dec 18, 2017 at 20:02
  • \$\begingroup\$ @phflack, fair enough. curr <= ptrLastElement is definitely more readable and it doesn't require computation. \$\endgroup\$
    – Classiest
    Dec 18, 2017 at 20:11
  • 2
    \$\begingroup\$ @Classiest You shouldn't worry about "computation" in this type of context, you can be absolutely certain the compiler will figure out the best way to accomplish what you intend to do. \$\endgroup\$
    – user128454
    Dec 18, 2017 at 20:47

2 Answers 2

9
\$\begingroup\$

It's not terrible as it stands, but the code could be improved. Here are some ideas on how to do so.

Minimize work within the loop

If we redefine ptrLastElement to something like this:

const int *end = &arr[SIZE];

Since the pointer starts from the beginning of the array, the for loop could instead look like this:

for (int * curr = arr; curr != end; ++curr)

This matches the use of real iterators in the standard library by having the end of the array be one beyond the last element.

However, modern C++ rarely uses pointers, so I would probably neither use nor advise anyone else to use such a construct. There's little benefit and the use of pointers can lead to difficult-to-find errors.

Don't use std::endl if '\n' will do

Using std::endl emits a \n and flushes the stream. Unless you really need the stream flushed, you can improve the performance of the code by simply emitting '\n' instead of using the potentially more computationally costly std::endl.

Omit return 0

When a C or C++ program reaches the end of main the compiler will automatically generate code to return 0, so there is no need to put return 0; explicitly at the end of main.

Note: when I make this suggestion, it's almost invariably followed by one of two kinds of comments: "I didn't know that." or "That's bad advice!" My rationale is that it's safe and useful to rely on compiler behavior explicitly supported by the standard. For C, since C99; see ISO/IEC 9899:1999 section 5.1.2.2.3:

[...] a return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument; reaching the } that terminates the main function returns a value of 0.

For C++, since the first standard in 1998; see ISO/IEC 14882:1998 section 3.6.1:

If control reaches the end of main without encountering a return statement, the effect is that of executing return 0;

All versions of both standards since then (C99 and C++98) have maintained the same idea. We rely on automatically generated member functions in C++, and few people write explicit return; statements at the end of a void function. Reasons against omitting seem to boil down to "it looks weird". If, like me, you're curious about the rationale for the change to the C standard read this question. Also note that in the early 1990s this was considered "sloppy practice" because it was undefined behavior (although widely supported) at the time.

So I advocate omitting it; others disagree (often vehemently!) In any case, if you encounter code that omits it, you'll know that it's explicitly supported by the standard and you'll know what it means.

\$\endgroup\$
5
  • \$\begingroup\$ Isn't your suggestion of &arr[SIZE] UB? or is this safe, because you are just accessing the address of a non existing object? \$\endgroup\$
    – DNKpp
    Dec 18, 2017 at 9:14
  • \$\begingroup\$ @DNK, it is allowed to take address of the one past the end element. Dereferencing it is UB. Going further is UB. \$\endgroup\$ Dec 18, 2017 at 9:15
  • \$\begingroup\$ @Edward, I noticed my programs work perfectly fine when return 0 is omitted, but I didn't know why. Now I do! Thanks. Also, you mentioned pointers are rarely used in modern C++, could you possibly explain why and what are used instead? \$\endgroup\$
    – Classiest
    Dec 18, 2017 at 20:41
  • 1
    \$\begingroup\$ @Classiest: Glad to be of help. Rather than pointers, it's more common in modern C++ to use references or const references. The difference is that a raw pointer can point to anything, including nullptr but a reference is only allowed to point to an actual object and so is much safer and easier to use. \$\endgroup\$
    – Edward
    Dec 18, 2017 at 21:51
  • 1
    \$\begingroup\$ I think that arr + SIZE is clearer than &arr[SIZE]. Probably a personal preference, like explicitly writing return 0 at the end of main. :) \$\endgroup\$ Dec 19, 2017 at 0:13
6
\$\begingroup\$

Misconception

At least in C++ community, index is not considered to be iterator. In fact, using std::iterator_traits<> on int will cause compilation error. Though pointer is iterator, but not every iterator is a pointer.

C

Usually people prefer iterators if they're easily accessible and simplify the problem. IIRC even in C people prefer \$[begin, end)\$ ranges, e.g. one past the end of the array. It is specifically allowed in C standard to go one past the end of array, but no further.

Application of the concepts above will give you what Edward covered in the post.

Iterators

One can #include <iterator> to get access to std::begin() and std::end(). Usually iterators are paired with #include <algorithm> to provide access to higher level algorithms. Generally one should use higher level algorithms if they don't need control the algorithms take away from them.

Algorithms

std::copy(std::begin(arr), std::end(arr),
          std::ostream_iterator<int>{std::cout, " "});

Now, that doesn't do the same thing as in your post (it doesn't print address), but it is what I use on a daily basis. My IDE has special shortcuts for this, especially for std::begin/std::end combination, and for std::ostream_iterator<int>.


The array filling loop can be rewritten using std::iota (from <numeric>):

std::iota(std::begin(arr), std::end(arr), 0);

Why standard algorithms and iterators?

The combination makes your code very flexible. If you'll decide to change the type to std::vector<int>, or std::list<int> (please don't use list unless there is a serious reason for it), your code will still work as expected.

constexpr

const is good, but constexpr will be better in this case. The feature is specifically designed for these use cases. Basically, just add expr at the end of const.

Puttin together

With all of the above, the code becomes:

#include <iostream>
#include <iterator>
#include <algorithm>
#include <numeric>
#include <array>

int main()
{
    constexpr size_t size = 10;
    std::array<int, size> arr;

    std::iota(std::begin(arr), std::end(arr), 0);
    std::copy(std::begin(arr), std::end(arr),
              std::ostream_iterator<int>{std::cout, " "});
}

Further use of standard library

C++11 has <array>, which is inside plain array, e.g. it doesn't have other non-static members, alignment is handled correctly as well. The syntax would be:

std::array<int, size> arr;

The benefits it gives are:

  • No decaying into pointer

  • Store their sizes (it is constexpr)

  • mimics std::vector's interface

  • tuple operations can be applied on std::array

Drawbacks:

  • Does not decay into pointer (will require template functions, which might lead to binary bloat)

  • somewhat more verbose

\$\endgroup\$
11
  • 1
    \$\begingroup\$ As much as I love the standard algorithms, I wouldn't personally recommend using std::copy into a std::ostream_iterator. I find it non-obvious / harder to read. A simple loop writing to std::cout is trivial for me to understand, whereas when I see this, I have to first think, "What is this trying to do?" Of course, this comes down to personal preference. Some people like it, some people don't \$\endgroup\$
    – Justin
    Dec 18, 2017 at 9:05
  • \$\begingroup\$ @Justin, I found that it depends on age. "New generation" (I define them as people who learned C++14 first and foremost) generally are better with higher level algorithms. Personally I look at output iterator first in std::copy(), so that it reads copy into <output> range <first-last>. That is why I usually have it on separate line. Though I do agree that it could be less verbose. \$\endgroup\$ Dec 18, 2017 at 9:07
  • 1
    \$\begingroup\$ You could go one step further toward modern C++ and use std::array<int, SIZE> arr; \$\endgroup\$
    – Edward
    Dec 18, 2017 at 9:18
  • 1
    \$\begingroup\$ @Edward, personally I don't like them very much. It is more verbose, and in this case it will add no direct benefit, as I use non-member being/end. I'll add it, but probably at the very end as a thing to consider. \$\endgroup\$ Dec 18, 2017 at 9:19
  • 1
    \$\begingroup\$ @Classiest Thanks. this seems to be a good starter about the topic. \$\endgroup\$ Dec 18, 2017 at 21:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.