The std::tuple template calss is a powerful tool since it's a generalization of std::pair. In particular, it defines a fixed-size collection of heterogeneous values [1].

The greatest benefitsprovided by std::tuple are:

  • Wrapping data
  • Restricting operations
  • Achiving more compact code

A simple use case

Let us assume that we have inherited the following code:

#include <tuple>
// [...]

void run(const Trajectory &i_trajectory) {
  // [...]

  // 1. get trajectory params
  const int numberOfPoints = static_cast<int>(orbitMap.size());
  const Trajectory::Time startTime = orbitMap.cbegin()->first;
  const Trajectory::Time endTime = orbitMap.crbegin()->first;

  // [...]

  // 2. perform interpolation
  Trajectory interpTrajectory = buildInterpolatedTrajectory(
      interpOrder, interpDeltaTime, numberOfPoints, startTime, endTime);

  // [...]

  // 3. get trajectory params again
  const int interpNumberOfPoints = static_cast<int>(interpTrajectory.size());
  const Trajectory::Time interpStartTime = interpTrajectory.cbegin()->first;
  const Trajectory::Time interpEndTime = interpTrajectory.crbegin()->first;

  MachineTrajectoryInfo machineTrajectoryInfo;

  // 4. pass the trajectory params to another object
  machineTrajectoryInfo.setNumberOfSamples(numberOfPoints);
  machineTrajectoryInfo.setStartTime(startTime);
  machineTrajectoryInfo.setEndTime(endTime);

  // [...]
}

where the Trajectory class and the buildInterpolatedTrajectory are definited as follow:

#include <array>
#include <map>

class Trajectory {
public:
  struct PointInfo {
    PointInfo(const std::array<double, 3> i_position,
              const std::array<double, 3> i_velocity);

    std::array<double, 3> position;
    std::array<double, 3> velocity;
  };

  using Time = double;
  using DataPoint = std::pair<Time, PointInfo>;
  using TrajectoryMap = std::map<Time, PointInfo>;

  Trajectory() = default;
  Trajectory(const TrajectoryMap &i_trajectoryMap);

  void addPoint(const DataPoint &i_trajectoryPoint);

  const TrajectoryMap &getTrajectoryMap() const;

private:
  TrajectoryMap m_trajectoryMap;
};

Trajectory buildInterpolatedTrajectory(int interpOrder, double interpDeltaTime,
                                       int numberOfPoints, double startTime,
                                       double endTime);

In addition, we know that

  • the run function is a legacy fucntion with a multiple operations, and multiple lines of code (of course it is not a good practice have big functions, but legacy code can be infamous).
  • the operations 1,2,3, and 4 are not the core operations of the function, but they are necessary for the flow of the program.

We want to improve the code readability and the flow of the program, wrapping toghether the trajectory parameters (numberOfPoints, startTime, endTime). One option is to wrap the Trajectory params in a data structure.

Starting form C++11 we can introduce in our code the std::tuple to wrap heterogeneous data.

#include <tuple>
// [...]

void run(const Trajectory &i_trajectory) {
  // [...]

  // A. enum to improve code readability
  enum ETrajectoryParams {
    eTrajectoryParams_NumberOfPoints = 0,
    eTrajectoryParams_StartTime,
    eTrajectoryParams_EndTime
  };

  // B. define a type alias for the tuple
  using TrajectoryParams = std::tuple<are_core::UInt32, are_core::datation::Mjd,
                                      are_core::datation::Mjd>;

  // C. Introduce a lambda function that retrieve the trajectory parameters
  const auto getTrajectoryParams =
      [](const Trajectory &i_trajectory) -> TrajectoryParams {
    const Trajectory::TrajectoryMap &trajectoryMap =
        i_trajectory.getTrajectoryMap();

    const int numberOfPoints = static_cast<int>(trajectoryMap.size());
    const Trajectory::Time startTime = trajectoryMap.cbegin()->first;
    const Trajectory::Time endTIme = trajectoryMap.crbegin()->first;

    return {numberOfPoints, startTime, startTime};
  };

  // 1. get trajectory params
  TrajectoryParams trajectoryParams getTrajectoryParams(i_trajectory);

  //[...]

  // 2. perform interpolation
  Trajectory interpTrajectory = computeInterpolation(
      interpOrder, interpDeltaTime, numberOfPoints, startTime, endTime);

  // [...]

  // 3. get trajectory params again
  TrajectoryParams interpTrajectoryParams getTrajectoryParams(i_trajectory);

  // 4. pass the trajectory params to another object
  MachineTrajectoryInfo machineTrajectoryInfo;

  machineTrajectoryInfo.setNumberOfSamples(numberOfPoints);
  machineTrajectoryInfo.setStartTime(startTime);
  machineTrajectoryInfo.setEndTime(endTime);

  // [...]
}

In order to introduce the std::tuple, we have added two important entities:

  1. A local enum to make easier the params retrivial and more expressive.
  2. A local type alias to make the code more meaningful.

These two entities are very useful if you work in a big codebase shared with other developers. Indeed, their help to understand the meaning of the semantic flow of the program explicitating the intentions with the appropriate names.

The last added entity is:

  1. A local lambda function to avoid duplicated operation.

All these three entities are definited locally since we don't want to expose these litte details in another.

Alternative solution

A common alternative solution can be use a POD struct. But it is a more powerful tool since a beginner developer can extend the struct with methods, violating two design principles:

  • Single responsability principle: use the data wrapper only for a precise intent: store data.
  • Open-close principle: restrict the data wrapper to only store data. Add no more features.

Although a POD class can be implemented to solve the prevous problem, it is a more powerful tool. Indeed, a struct offer more extension for example a beginner programmer can think to add a method to this struct vaiolating the single resposability principle.

Final Thoughts

In my huble opinion, in general, I prefer to use:

  • The std::tuple complemented with an enum and an alias to make the code more readable. Moreover, I prefer to implement them within functions, as a detailed structure; in fact enum and std::tupleare not a single entity, one risks using them separately, losing the meaning for which they were implemented.
  • The POD struct in all other cases.

Be awere that between std::tuple and structs there are a few implementation details which can became critical in some situations. See the the article here about POD struct vs tuple.

References

[1]: The std::tuple definition https://en.cppreference.com/w/cpp/utility/tuple


Published

Category

Tips&Tricks

Tags

Contact