Recurrent minor issues in C++ code: Part 2

17 Apr 2021 - John Z. Li

While reading real-world C++ code in some open-source projects, I noticed the following minor issues appearing again and again: (read the first part of this post here.

Move an object returning it from a function

Consider the following code:

std::vector<int> my_fun(void)
{
    std::vector<int> vec {1,2,3,4,5};
    return std::move(vec);
}

This kind of code appears frequently in projects I have encountered. The intention of programmers who do this that they wants to accelerate return by avoiding unnecessary copies. But this actually make the code slower.

Starting from C++17, Return Value Optimization (RVO) is now mandatory. This means, if the code is written as ` return std::vector{1, 2, 3, 4, 5}; ` no copy or move is performed. The compiler guarantees that the vector is constructed at the caller’s site. If the returned variable is given a name first as in the first code sample, Named Return Value Optimization (NRVO) might kick in. This also will leads to no copy or move being needed. Though NRVO, unlike RVO, is not mandatory, most compilers have implemented it as an optimization. Performance-wise, RVO and NRVO are even better than move-semantics. Using std::move to a variable before it is returned prohibits RVO or NRVO, thus, making the program slower.

Even if RVO and NRVO are not applicable in certain cases. The compiler is smart enough to invoke the move constructor of the variable to construct the return statement. A copy constructor will only be used a move constructor is not available.

Use std::move to const objects

Consider the following code:

  const std::vector<cv::Point2i> box_corner_points =
      GetAffinedBoxImgIdx(0.0, 0.0, M_PI_2,
                          {
                              std::make_pair(param.front_edge_to_center(),
                                             param.left_edge_to_center()),
                              std::make_pair(param.front_edge_to_center(),
                                             -param.right_edge_to_center()),
                              std::make_pair(-param.back_edge_to_center(),
                                             -param.right_edge_to_center()),
                              std::make_pair(-param.back_edge_to_center(),
                                             param.left_edge_to_center()),
                          },
                          0.0, 0.0, 0.0);
  cv::fillPoly(
      *img_feature,
      std::vector<std::vector<cv::Point>>({std::move(box_corner_points)}),
      color);

In the above code, box_corner_points is first created as a const variable, then it is moved to construct a temporary. The intention of the programmer is to use the move constructor to initialize the temporary. But there are two problems regarding this kind of usage of std::move.

Bad floating point comparison

Consider the following code:

template <class T>
typename std::enable_if<!std::numeric_limits<T>::is_integer, bool>::type
almost_equal(T x, T y, int ulp) {
  // the machine epsilon has to be scaled to the magnitude of the values used
  // and multiplied by the desired precision in ULPs (units in the last place)
  // unless the result is subnormal
  return std::fabs(x - y) <=
             std::numeric_limits<T>::epsilon() * std::fabs(x + y) * ulp ||
         std::fabs(x - y) < std::numeric_limits<T>::min();
}

This function does one simple thing: to compare two floating point numbers to see whether they are close enough up to an given scaling factor. How hard could be that, you may ask. Let us find out:

The correct way to implement the function is like below:

double almost_equal(double x, double y, unsigned int ulp =1){
	assert(std::isfinite(x) && std::isfinite(y));
	return fabs(x - y) <= std::numeric_limits<double>::epsilon * std::max(ulp,  1)
			|| fabs(x/y -1) <= std::numeric_limits<double>::epsilon * std::max(ulp,  1);
}

When x and y are near zero, the comparison is handled by fabs(x - y), otherwise, it is handled by fabs(x/y -1. The latter comparison will lead to the correct result even if y==0. We also have to make sure that ulp >=1.

Unnecessary memset

Consider the following code:

  float flipped_anchors_dx[kNumAnchor];
  float flipped_anchors_dy[kNumAnchor];
  memset(flipped_anchors_dx, 0, kNumAnchor * sizeof(float));
  memset(flipped_anchors_dy, 0, kNumAnchor * sizeof(float));

Arrays are aggregate types in C++. float my_array[MY_SIZE]{} or float my_array[MY_SIZE] = {} leads to all elements of the array are set to zero.

Two things to notice here:

Note for C programming, one should use `float my_array[MY_SIZE] = {0} to define an array with all elements being zero.