5
\$\begingroup\$

I have written a cpp program which demonstrates the use of the following functionalities in modern cpp.

Lambda Functions
std::generate
std::vector
std::erase
std::remove_if
std::remove
std::max_element
std::for_each
std::shuffle

I want to understand the following

(1) Have I written the code properly or there are somethings which needs to be improved.

(2) Will it be helpful if I write it as a blog for others. If yes, do I need to add more comments etc to make it more consumable?

 /*
 * Copyright Rishi Agrawal <rishi.b.agrawal@gmail.com>
 *
 * The idea behind this program is to understand how to use the following.
 * Lambda Functions
 * std::generate
 * std::vector
 * std::erase
 * std::remove_if
 * std::remove
 * std::max_element
 * std::for_each
 * std::shuffle
 */
#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <cassert>

void printInt(int n) {
  std::cout << " " << n;
}

/**
 * @brief Function to demonstrate lambda functions. 
 *
 * @param v - the vector to use.
 */
void playingWithLambdaFunctions(std::vector<int> &v) {
  /* Generate the elements in the vector. The lambda function take a variable
   * and increments it everytime. Using mutable so that the value of the
   * variable is preserved accross invocations of lambda.*/

  std::generate(v.begin(), v.end(), [n = 0]() mutable { n = n + 5; return n;});

  /* Print the vector using a lambda function. Here the value at a particular
   * position is passed as n. This passing is by value. */

  std::for_each(v.begin(), v.end(), [](int n) { std::cout << " " << n; });

  /* Other way of printing where we use a unary function for printing. */
  std::cerr << "\n\nPrinting using for_each and unary_function\n\n";
  std::for_each(v.begin(), v.end(), printInt);

  std::cerr << "\n\nPrinting and increasing values of the passed integers. "\
                    "This will not change the sequence as the values are "
                    "passed by value.\n\n";

  std::for_each(v.begin(), v.end(), [](int n) {
                                      std::cout << " CurrentVal(" << n << ")";
                                      n = n + 1;  // Changing the value.
                                      std::cout << " ChangedVal(" << n << ")";
                                      });

  std::cerr << "\n\nPrinting to check if the values changed.\n\n";
  std::for_each(v.begin(), v.end(), printInt);

  std::cerr << "\n\nPrinting and increasing values of the passed integers. "\
                    "This will CHANGE the values as we are passing the "
                    "numbers by reference.\n\n";

  std::for_each(v.begin(), v.end(), [](int &n) {
                                      std::cout << " CurrentVal(" << n << ")";
                                      n = n + 1;  // Changing the value.
                                      std::cout << " ChangedVal(" << n << ")";
                                      });

  std::cerr << "\n\nPrinting to check if the values changed.\n\n";
  std::for_each(v.begin(), v.end(), printInt);

  std::cerr << "\n\nIncreasing values of the passed integers in the vector. "\
                    "This will CHANGE the values as we are passing the "
                    "numbers by reference. This time we are also passing the "
                    "value with which we want to increase.\n\n";

  int increment = 5;
  std::for_each(v.begin(), v.end(), [increment](int &n) {
                                      std::cout << " CurrentVal(" << n << ")";
                                      n = n + increment;  // Changing the value.
                                      std::cout << " ChangedVal(" << n << ")";
                                      });

  std::cerr << "\n\nPrinting to check if the values changed.\n\n";
  std::for_each(v.begin(), v.end(), printInt);

  std::cerr << "\n\nConverting all the odd numbers to even by increasing by 1."\
                    " This will CHANGE the values as we are passing the "
                    "numbers by reference.\n\n";

  std::for_each(v.begin(), v.end(), [](int &n) { if (n%2) { n++; } });
  std::cerr << "\n\nPrinting to check if the values changedX.\n\n";
  std::for_each(v.begin(), v.end(), printInt);
}

/**
 * @brief Do some more operations like remove_if, and remove on vector.
 *
 * @param v - the vector to use.
 */
void otherSequenceOperations(std::vector<int> &v) {
  /* Remove the elements which are divisible by 4 */
  std::sort(v.begin(), v.end());
  std::cerr << "\n\nPrinting to check if the numbers are sorted.\n\n";
  std::for_each(v.begin(), v.end(), printInt);

  std::cout << "\n\nRemoving all the numbers which are divisible by 4";
  auto eraseBegin = std::remove_if(v.begin(), v.end(), [](const int n) {
                                      if (n%4 == 0) {
                                        return true;
                                      } else {
                                        return false;
                                      }
                                    });

  /* std::remove_if moves the removable elements to the end of the list and
   * returns the iterator to the begining of the elements to be removed. */
  v.erase(eraseBegin, v.end());

  std::cerr << "\n\nPrinting to check if the numbers are removed.\n\n";
  std::for_each(v.begin(), v.end(), printInt);

  std::cout << "\nAdding some more elements";
  v.push_back(10);
  v.push_back(20);
  v.push_back(30);
  v.push_back(40);
  v.push_back(50);

  std::cout << "\nPrinting the freshly added numbers";
  std::for_each(v.begin(), v.end(), printInt);

  /* Remove the elements which is equal to 22. */
  int numberToRemove = 22;
  eraseBegin = std::remove(v.begin(), v.end(), numberToRemove);
  v.erase(eraseBegin, v.end());

  std::cerr << "\n\nPrinting to check if 22 was removed. We are counting the "
                "presence of the number 22 in the vector. If it is not 0, we "
                " exit.\n\n";
  assert(0 == count(v.begin(), v.end(), numberToRemove));
  std::for_each(v.begin(), v.end(), printInt);
}

/**
 * @brief Function to demonstrate the std::max_element.
 *
 * @param v - the vector to be used.
 */
void playWithMaxElement(std::vector <int> &v) {
  auto maxElementIt = std::max_element(v.begin(), v.end());
  auto maxElement = *(maxElementIt);

  /* Replace the max element with -1.*/
  std::replace(v.begin(), v.end(), maxElement, -1);
  std::cout << "\n\nPrinting after replacing " << maxElement << " with "\
                  << -1 << "\n";
  std::for_each(v.begin(), v.end(), printInt);
  std::cout << "\n";

  /* Replace the max element in one line. */

  std::replace(v.begin(),
                v.end(),
                *(std::max_element(v.begin(), v.end())),  // Return the position
                                                         // of the max element.
              100);

  std::cout << "\n\nPrinting after replacing max element with 100 \n";
  std::for_each(v.begin(), v.end(), printInt);
  std::cout << "\n";

  /* Writing our max function */
  int ourMax = 0;

  v.push_back(std::numeric_limits<int>::max());
  std::for_each(v.begin(), v.end(), [&ourMax](int n) {
                                      if (n > ourMax) {
                                        ourMax = n;
                                      }
                                    });
  std::cerr << "\nOur max is " << ourMax;
  assert(ourMax == std::numeric_limits<int>::max());
}

/**
 * @brief Shuffles the vector
 *
 * @param v - the vector to be shuffled.
 */
void shuffleVector(std::vector <int> &v) {
  std::random_device rd;
  std::mt19937 g(rd());

  std::shuffle(v.begin(), v.end(), g);
  std::cerr << "\n\nPrinting the shuffled vector.\n\n";
  std::for_each(v.begin(), v.end(), printInt);
}

int main() {
  int sizeOfVector = 10;

  /* Create the vector. */
  std::vector <int> v = std::vector <int>(sizeOfVector);
  std::cout << "\nPrint the created vector";
  std::for_each(v.begin(), v.end(), printInt);

  std::cout <<"\nDoing some operations in the vector";
  playingWithLambdaFunctions(v);
  otherSequenceOperations(v);
  shuffleVector(v);
  playWithMaxElement(v);
  return 0;
}

Makefile is

CPP_STANDARD="c++17"
STANDARD_FLAG="-std=$(CPP_STANDARD)"
OUTPUT_FILE="exe_lambda"
OUTPUT_FLAG=-o $(OUTPUT_FILE)
WARNINGS_FLAG=-Wall -Werror
CPPFLAGS=$(STANDARD_FLAG) $(OUTPUT_FLAG) $(WARNINGS_FLAG)
CC=g++
SOURCE_CODE=lambdas.cc

all:
    $(CC) $(CPPFLAGS) $(SOURCE_CODE)

run: all
    ./$(OUTPUT_FILE)

Output is

Print the created vector 0 0 0 0 0 0 0 0 0 0 Doing some operations in the vector 5 10 15 20 25 30 35 40 45 50

Printing using for_each and unary_function

5 10 15 20 25 30 35 40 45 50

Printing and increasing values of the passed integers. This will not change the sequence as the values are passed by value.

CurrentVal(5) ChangedVal(6) CurrentVal(10) ChangedVal(11) CurrentVal(15) ChangedVal(16) CurrentVal(20) ChangedVal(21) CurrentVal(25) ChangedVal(26) CurrentVal(30) ChangedVal(31) CurrentVal(35) ChangedVal(36) CurrentVal(40) ChangedVal(41) CurrentVal(45) ChangedVal(46) CurrentVal(50) ChangedVal(51)

Printing to check if the values changed.

5 10 15 20 25 30 35 40 45 50

Printing and increasing values of the passed integers. This will CHANGE the values as we are passing the numbers by reference.

CurrentVal(5) ChangedVal(6) CurrentVal(10) ChangedVal(11) CurrentVal(15) ChangedVal(16) CurrentVal(20) ChangedVal(21) CurrentVal(25) ChangedVal(26) CurrentVal(30) ChangedVal(31) CurrentVal(35) ChangedVal(36) CurrentVal(40) ChangedVal(41) CurrentVal(45) ChangedVal(46) CurrentVal(50) ChangedVal(51)

Printing to check if the values changed.

6 11 16 21 26 31 36 41 46 51

Increasing values of the passed integers in the vector. This will CHANGE the values as we are passing the numbers by reference. This time we are also passing the value with which we want to increase.

CurrentVal(6) ChangedVal(11) CurrentVal(11) ChangedVal(16) CurrentVal(16) ChangedVal(21) CurrentVal(21) ChangedVal(26) CurrentVal(26) ChangedVal(31) CurrentVal(31) ChangedVal(36) CurrentVal(36) ChangedVal(41) CurrentVal(41) ChangedVal(46) CurrentVal(46) ChangedVal(51) CurrentVal(51) ChangedVal(56)

Printing to check if the values changed.

11 16 21 26 31 36 41 46 51 56

Converting all the odd numbers to even by increasing by 1. This will CHANGE the values as we are passing the numbers by reference.

Printing to check if the values changedX.

12 16 22 26 32 36 42 46 52 56

Printing to check if the numbers are sorted.

12 16 22 26 32 36 42 46 52 56

Removing all the numbers which are divisible by 4

Printing to check if the numbers are removed.

22 26 42 46 Adding some more elements Printing the freshly added numbers 22 26 42 46 10 20 30 40 50

Printing to check if 22 was removed. We are counting the presence of the number 22 in the vector. If it is not 0, we exit.

26 42 46 10 20 30 40 50

Printing the shuffled vector.

10 30 46 20 26 40 50 42

Printing after replacing 50 with -1 10 30 46 20 26 40 -1 42

Printing after replacing max element with 100 10 30 100 20 26 40 -1 42

Our max is 2147483647

\$\endgroup\$
9
  • 2
    \$\begingroup\$ Which version of C++ are you targeting? \$\endgroup\$
    – nwp
    Apr 17 '18 at 12:48
  • 1
    \$\begingroup\$ You use std:for_each way too much. for (auto& elem: vec) ... is better unless: 1) for_each's last argument is an existing function (like std::for_each(v.begin(), v.end(), printInt);) or 2) you want to iterate over n elements :std::for_each(v.begin(), v.begin()+3, [](auto i) { std::cout << i*i; }); \$\endgroup\$
    – papagaga
    Apr 17 '18 at 13:49
  • \$\begingroup\$ A blog on <algorithm> isn't a bad idea, but you must aim for something more consistent than these examples, e.g: which king of "raw loops" you can replace with algorithms, which algorithm you should choose, etc. \$\endgroup\$
    – papagaga
    Apr 17 '18 at 13:53
  • \$\begingroup\$ I notice the copyright notice in your code. Which is fine. But I should also note that by posting on this site you are granting a license for the code to be used (see the bottom of the page) contributions licensed under cc by-sa 3.0 Link: cc by-sa 3.0 \$\endgroup\$ Apr 17 '18 at 15:10
  • \$\begingroup\$ Sure you can write an article about these. But you are missing the more important algorithms. std::find() and std::transform(). The std::for_each() has been made a bit redundant with range based for. But I think you would be missing the point by just talking about these. Its the combination of algorithm with iterator that makes them interesting. Look at some of the interesting iterators (not just the container iterators) std::istream_iterator and std::ostream_iterator \$\endgroup\$ Apr 17 '18 at 15:15
5
\$\begingroup\$

For such a simple case this Makefile is a waste.

It can be replaced with:

> rm Makefile
> export CXXFLAGS ="-std=c++17 -Wall -Werror"
> make lambdas

The standard implicit rules of Make will build lambdas from lambdas.cc

Sure Good use:

  std::generate(v.begin(), v.end(), [n = 0]() mutable { n = n + 5; return n;});

Probably not a good example:

  std::for_each(v.begin(), v.end(),
                [](int n) { std::cout << " " << n; });

I like (as it shows intent better).

  std::copy(v.begin(), v.end(),
            std::ostream_iterator<int>(std::cout, "\n");

But I think the modern (and idiomatic) form is:

  for(auto const& item: v) {
      std::cout << v << "\n";
  }

I like this even less:

  std::for_each(v.begin(), v.end(), printInt);

You could have just as easily written:

  printVect(v);

Here the lambda is too large. At this size you should be creating a functor. If you have to do it inline then don't indent it all the way to the right like that. Some people don't have wide screens.

   std::for_each(v.begin(), v.end(), [](int n) {
                                      std::cout << " CurrentVal(" << n << ")";
                                      n = n + 1;  // Changing the value.
                                      std::cout << " ChangedVal(" << n << ")";
                                      });

OK. I'll ignore the rest of the for_each as I think we have covered the main points.

Good:

  std::sort(v.begin(), v.end());

Erase/Remove is a good example. Though this lambda is terrible.

  auto eraseBegin = std::remove_if(v.begin(), v.end(), [](const int n) {
                                      if (n%4 == 0) {
                                        return true;
                                      } else {
                                        return false;
                                      }
                                    });

Don't use a if statement to generate a true/false result. That value is an inherent part of the expression.

     if (n%4 == 0) {
         return true;
     } else {
         return false;
     }

Can be simplified to:

     return n%4 == 0;

Now that's a good lambda.

  auto eraseBegin = std::remove_if(v.begin(), v.end(),
                         [](const int n) {return n%4 == 0;});
  v.erase(eraseBegin, v.end());

Good:

  eraseBegin = std::remove(v.begin(), v.end(), numberToRemove);
  v.erase(eraseBegin, v.end());

Good:

  auto maxElementIt = std::max_element(v.begin(), v.end());
  auto maxElement = *(maxElementIt);

Good:

  std::replace(v.begin(), v.end(), maxElement, -1);

I like this: it's a good simple example of random usage.

void shuffleVector(std::vector <int> &v) {
  std::random_device rd;
  std::mt19937 g(rd());

  std::shuffle(v.begin(), v.end(), g);
  std::cerr << "\n\nPrinting the shuffled vector.\n\n";
  std::for_each(v.begin(), v.end(), printInt);
}

That random number generator is very expensive to create. And once you have it up and running, why recreate it each time? So I would make the random number stuff static inside the function:

void shuffleVector(std::vector <int> &v) {
  static std::random_device rd;
  static std::mt19937 g(rd());

As a side note:

The standard introduced std::begin() and std::end(). So you can now use these rather than the member versions. This allows you to generalize the code so it can be used on any container type (including arrays).

using std::begin;
using std::end;
std::for_each(begin(v), end(v), doStuff);

Now you don't need to care about the type of v. You can even change it to a C-Array and it would still work.

\$\endgroup\$
6
  • \$\begingroup\$ I apologize if I'm wrong but beyond being expensive to create doesn't a PRNG need to be a single reusable instance like you suggested in order to work properly? \$\endgroup\$
    – Summer
    Apr 17 '18 at 22:09
  • \$\begingroup\$ @bruglesco The new random allows you to create multiple random number generators and they should work independently (and I hear well). But in my personal opinion: One should be enough for an app. \$\endgroup\$ Apr 17 '18 at 23:06
  • \$\begingroup\$ Doesn’t non-member begin and end need to be unqualified to really be universal? std::begin will work for a primitive array and std collections, but not for custom collections and ranges from 3rd party libraries that are not in the std namespace. You need to use ADL, just like with swap. \$\endgroup\$
    – JDługosz
    Apr 18 '18 at 6:30
  • \$\begingroup\$ Thanks a lot for reviewing in so much detail. Thanks for the useful comments. I will incorporate the suggested changes. I got to learn a lot. :-) \$\endgroup\$ Apr 18 '18 at 7:24
  • \$\begingroup\$ @JDługosz I see what you are saying. Do be honest I had not thought about that, but its a good point. You are saying for types that don't have a begin/end method but do have specializations of the begin/end free standing methods. Personally I would hope that if you implement iterator like semantics for your object you also implement the begin/end method, but I can see how it is not required. \$\endgroup\$ Apr 18 '18 at 16:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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