Search code examples
c++templatesrefactoring

How to refactor type-specific but almost same redundant operations without full class templating


I'm working on a C++ class that needs to perform type-specific operations on different data structures. For specific reasons, making class template parameterized is not a solution for me.

but I'm struggling to find a clean solution that doesn't involve significant code duplication. Here's a simplified version of my current code. Note that in do_something I will write and delete the cache, also get reference of some value of the cache and do computation using it.

Do you have any suggestion to refactor the class?

#include <vector>
// Note that I dont want to use a template class
struct DataBase {
  std::vector<double> cached; // std::vector is just an example. 
  std::vector<double> cache2d;
  std::vector<float> cachef;
  std::vector<float> cache2f;
  // many more data structures with different types (float or double)
  template <typename T>
  void do_something() {
    if (std::is_same<T, double>::value) {
      // long lines of code using cached or cache2d
      // in my actual code, I have a lot of these like cache3d, cache4d, etc.
      // and I use all of them in the function.
    } else if (std::is_same<T, float>::value) {
      // all the same long lines of code as above
      // except using float version of caches rather than double
    }
  }
};
int main() {
  DataBase db;
  db.do_something<double>();
  db.do_something<float>();
  return 0;
}

Solution

  • I would suggest you decouple the real data source with the operations. I would do something like:

    struct DataBase {
      std::vector<double> cached;
      std::vector<double> cache2d;
      std::vector<float> cachef;
      std::vector<float> cache2f;
    };
    
    template <typename T>
    struct DbAccessor {
      std::vector<T>& cache;
      std::vector<T>& cache2;
    
      DbAccessor(std::vector<T>& c, std::vector<T>& c2) : cache(c), cache2(c2) {}
      
      void do_something() {
        // db operations to
      }
    };
    
    
    int main() {
      DataBase db;
    
      // here is the new complexity, but hopefully less complex than your do_something logic
    
      DbAccessor<double>(db.cached, db.cache2d).do_something();
      DbAccessor<float>(db.cachef, db.cache2f).do_something();
    
      
      return 0;
    }
    
    

    I think for a simple application above is a reasonable trade off. However, based on your usage pattern, I may argue that cached and cachef can be abstracted to begin with. Check out the following:

    template <typename T>
    struct TypedDatabase {
      std::vector<T> c;
      std::vector<T> c2;
    };
    
    struct Database {
       TypedDatabase<float> cachef;
       TypedDatabase<double> cached;
    
       template <typename T>
       TypedDatabase<T>& get_cache();
    
       template <>
       TypedDatabase<float>& get_cache() { return cachef; } 
    
       template <>
       TypedDatabase<double>& get_cache() { return cached; } 
    };
    
    // optionally write this as DbAccessor method
    template <typename T>
    void do_something(TypedDatabase<T>& cache) {
       // operate on cache.c and cache.c2
    }