Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using a Template to Expose a Private Typedef

I have a class that contains a private typedef and several member functions:

class Foo
{
private:
  typedef std::blahblah FooPart;
  FooPart m_fooPart;
  ...
public:
  int someFn1();
  int someFn2();
};

Several member functions need to use m_fooPart in a similar way, so I want to put that in a function. I put helper functions in the anonymous namespace whenever I can, but in this case, they need to know what FooPart is. So, I've done this:

namespace
{
  template <typename T>
  int helperFn(const T& foopart, int index)
  {
    ...
    return foopart.fn(index);
  }
}

int Foo::someFn1()
{
  ...
  return helperFn(m_fooPart, ix);       
}

By forcing the compiler to produce the FooPart type, am I still in the land of well-defined behavior? Is there a more elegant way of accomplishing this that doesn't increase the size of Foo or make public what is now private?

like image 379
criddell Avatar asked Feb 25 '23 16:02

criddell


2 Answers

Yes, that approach produces well-defined, standards-compliant behavior.

That said, adding member functions to a class does not increase the size of a class (assuming you mean the result of the sizeof operator), so I'm not sure what drawback you perceive in just making the helper function a private member of Foo.

like image 136
ildjarn Avatar answered Mar 08 '23 10:03

ildjarn


Simple answer: make the typedef public.

That will leak a minor detail of implementation (the actual internal type), but because it is typedefed you can redefine it at any time and it should be fine.

A little less simple: befriend the helper function, providing access to your internal type.

The problem with this second approach is that you are not only granting access to the typedef, but also to all the private parts of your class, and that might not be the best idea. At any rate, since this is an internal helper function, it is under your own control, and it should be fine. (Now that I think of it, you might want to declare the function in a named namespace, for the friend declaration to succeed)

Even less simple: Create a separate typedef inside the implementation file, and ensure that they are synchronized.

You can ensure that the types are the same with a small bit of metaprogramming, with a same_type<T,U> template that will provide a true value if the two types are the same and false otherwise. A static assert will trigger an error if the typedef changes in only one place

Back to simple again: provide the typedef or use the type directly without the static assert.

You are calling a function (this should not be a template as in your code) and passing a reference. If the typedef changes in the class, the call will fail and the compiler will tell you.

I would go for the last option, while it may look a little rough and less delicate than the others, the fact is that this is only an implementation detail that is not used by others, you are under full control of the code and well, simple is better.

EDIT, after the comment.

I started writing this as a comment, but it became too long, so I am adding it to the answer.

There is nothing wrong in that solution by itself, other than you are making a function generic unnecessarily and some error messages in the future might not be as simple as they could be with a non-generic signature. Note that the template will not expose the typedef (as the question title suggests) but rather it will make the compiler infer the type at the place of call.

If you change the typedef, instead of getting an error saying that the arguments to helperFn cannot be matched against the existing function, the type will be inferred and the function matched, but you will get an error deeper in helperFn if you use a property of the type that is no longer present. Or worse, you might not even get an error if it is the semantics of the type that have changed.

Consider that the typedef is of a std::list<X>, and that in the function you are iterating over it with this simple correct for loop:

for (typename T::iterator it=x.begin(), end=x.end(); it != end; ) {
   if ( condition(*it) ) 
      it = x.erase(it); 
   else 
      ++it; 
}

Can you catch the effect that changing the typedef to std::vector<X> will have? The compiler cannot even if the code is now incorrect. Whether writing the for loop like that is a good idea, or why is it not just using the erase-remove idiom are different issues (as a matter of fact the previous loop is arguably better than erase-remove for a list), the concrete situation is that the semantics have changed, and because the type is syntactically compatible with the previous one the compiler will not notice that the code is wrong, it will not point you to that function and chances are that you will not review/rewrite it.

like image 35
David Rodríguez - dribeas Avatar answered Mar 08 '23 10:03

David Rodríguez - dribeas