Mihai Todor
Mihai Todor

Reputation: 8249

Merging functions with similar content, but different parameters

Given the following (pseudo)code:

typedef std::map<const unsigned int, unsigned long int> ModelVector;
typedef std::vector<unsigned long int> EncryptedVector;

int test1 (const EncryptedVector &x)
{
    //compute ModelVector y
    data = kenel1(x, y);
    //compute output
}
int test2 (const EncryptedVector &xx)
{
    //compute ModelVector y
    data = kenel2(xx, y);
    //compute output
}
int test3 (const EncryptedVector &x, const EncryptedVector &xx)
{
    //compute ModelVector y
    data = kenel3(x, xx, y);
    //compute output
}
int test4 (const EncryptedVector &x, const EncryptedVector &xSquared)
{
    //compute ModelVector y
    data = kenel4(x, xSquared, y);
    //compute output
}

Because the variables y and output are computed identically in all 4 functions and since I have a "global" object which allows me to select the appropriate kernel function via a switch statement, I was wondering if there's a more elegant way to write them, preferably merging them somehow...

For example, something like this (pseudocode) would be a decent choice:

int test (const EncryptedVector &x, const EncryptedVector &xx, const EncryptedVector &xSquared)
{
    //compute ModelVector y
    //switch (kernel) -> select the appropriate one
    //compute output
}

test (x, NULL, NULL);//test1
test (NULL, xx, NULL);//test2
test (x, xx, NULL);//test3
test (x, NULL, xSquared);//test4

Or, even better, I could declare test multiple times with different parameter combinations, all falling back on the one above (even though I would loose the semantic distinction between x and xx).

The problem with the above approach is that C++ doesn't allow me to pass NULL instead of std::vector and I think I'm better off repeating the code 4 times than to start passing variables by pointer instead of passing them by reference...

Is there any other way of doing this?


EDIT: Here are the prototypes for the kernel functions:

int kernel1 (const EncryptedVector &x, const ModelVector &y);
int kernel2 (const EncryptedVector &xx, const ModelVector &y);
int kernel3 (const EncryptedVector &x, const EncryptedVector &xx, const ModelVector &y);
int kernel4 (const EncryptedVector &x, const EncryptedVector &xSquared, const ModelVector &y);

Upvotes: 1

Views: 113

Answers (2)

anatolyg
anatolyg

Reputation: 28269

You could send an object (it's called functor, if i'm not mistaken) that contains the additional parameters and has a function that calculates the kernel (kenel?).

// Main function, looks more generic now
template <class F>
int test1234(F functor)
{
    //compute ModelVector y
    data = functor(y);
    //compute output
}

...

// Functors involve some tedious coding, but at least your main calculation is clean
struct functor_for_kernel1
{
    const EncryptedVector &x;
    functor_for_kernel1(const EncryptedVector &x): x(x) {}
    int operator(const ModelVector &y) {return kernel1(x, y);
};

struct functor_for_kernel2 // easy
...

struct functor_for_kernel3
{
    const EncryptedVector &x, const EncryptedVector &xx;
    functor_for_kernel3(const EncryptedVector &x, const EncryptedVector &xx): x(x), xx(xx) {}
    int operator()(const ModelVector &y) {return kernel3(x, xx, y);
};

struct functor_for_kernel4 // easy
...

// Usage of the unified function
test1234(functor_for_kernel1(x));
test1234(functor_for_kernel2(xx));
test1234(functor_for_kernel3(x, xx));
test1234(functor_for_kernel4(x, xx));

Upvotes: 0

John Dibling
John Dibling

Reputation: 101456

Your original 4 test methods select a different behavior (eg, a different kernel function is called) based not on the types of the arguments passed, but by the semantics of the arguments passed. Not to metion the fact they have entirely different names.

If you could somehow change this parameterization from a semantic one to one of a type, you could use your proposed method of combining all 4 methods in to one.

This means having one function with all 3 parameters, which is exactly what you've proposed:

int test (const EncryptedVector &x, const EncryptedVector &xx, const EncryptedVector &xSquared)

...the problem of course being you can't pass NULL by reference. You need to pass an actual vector. You could pass an empty, temporary vector, like this:

test (x, EncryptedVector(), EncryptedVector());  //test1

...and then select the method internally based on the empty() state of the passed in vector:

int test (const EncryptedVector &x, const EncryptedVector &xx, const EncryptedVector &xSquared)
{
    //compute ModelVector y
    //switch (kernel) -> select the appropriate one
      if( xx.empty() && xSquared.empty() )
      {
        // kenel1 method
      }
      else if( x.empty() && xSquared.empty() )
      {
        // kenel2 method
      }
      else if( ... etc ... )

    //compute output
}

And perhaps for your uses this is sufficient, but the above raises at least a couple new issues.

One is the chain of if statements. This introduces complexity, both in factors of execution time, and perhaps more importantly in terms of maintainability. I know I wouldn't want to maintain 4 pages of if statements.

Other issues include the clumsy invocation of the method with empty vectors, and the fact that you can still call test with an invalid combination of vectors.

So, getting back to using a type-based selection of behavior rather than a semantic one. What if you didn't specify a concrete type for test()'s parameters, but a template instead?

template<class VectorX, class VectorXX, class VectorXSquared>
int test(const VectorX& x, const VectorXX& xx, const VectorXSquared& xSquared);

Now you can also provide a special NullVector type:

class NullVector {}; // Empty Class

... and then explicitly specialize the test function for each of your valid use cases (above, you have listed 4 valid use cases).

Invocation of test now becomes something like this:

test(x,NullVector(),NullVector());  // First Use Case

This also has an added benefit. If you do not provide an implementation for the non-specialized version of test, any attempt to call test with invalid parameters will fail to compile & link. For example, in your listed use cases, none of them take 3 valid EncryptedVector objects, so this should fail to compile:

test(x, xx, xSquared);

...which, indeed, it will if you don't provide a non-specialized implemtation of test.

OK, that's a lot of talking and maybe it wasn't all well explained. So here is a complete sample that I hope will help illustrate what I'm talking about:

#include <vector>
#include <string>
using namespace std;

typedef vector<string> EncryptedVector;

class NullVector{}; // Empty Class

int kernel1(const EncryptedVector& x)
{
    return 1;
}

int kernel2(const EncryptedVector& xx)
{
    return 2;
}

int kernel3(const EncryptedVector& x, const EncryptedVector& xx)
{
    return 3;
}

int kernel4(const EncryptedVector& x, const EncryptedVector& xSquared)
{
    return 4;
}

template<class VectorX, class VectorXX, class VectorXSquared> 
int test(const VectorX& x, const VectorXX& xx, const VectorXSquared& xSquared);

template<> int test<>(const EncryptedVector& x, const NullVector&, const NullVector&)
{
    return kernel1(x);
}

template<> int test<>(const NullVector&, const EncryptedVector& xx, const NullVector&)
{
    return kernel2(xx);
}

template<> int test<>(const EncryptedVector& x, const EncryptedVector& xx, const NullVector&)
{
    return kernel3(x, xx);
}

template<> int test<>(const EncryptedVector &x, const NullVector&, const EncryptedVector &xSquared)
{
    return kernel4(x,xSquared);
}

int main()
{
    EncryptedVector x, xx, xSquared;  // each somehow populated

    ///*** VALID USE-CASES ***///
    test(x,NullVector(),NullVector());
    test(NullVector(),xx,NullVector());
    test(x,xx,NullVector());
    test(x,NullVector(),xSquared);

    ///*** INVALID USE CASES WILL FAIL TO COMPILE&LINK ***///
    test(x,xx,xSquared);

}

Upvotes: 4

Related Questions