c.bear
c.bear

Reputation: 1445

Expose only class instances in C++

I'm trying to define a class where explicit instantiation is prohibited. The user must be able to use only a limited set of instances.

In term of code I'm currently doing something like this:

class Obj
{
private:
  int _x;
  Obj(int x) : _x() {}

public:
  static const Obj obj1;
  static const Obj obj2;
};

const Obj Obj::obj1(1);
const Obj Obj::obj2(2);

int main()
{
  Obj o1 = Obj::obj1;
}

The constructor is private and class instances are available thru Obj::xxx.

With this approach however, I don't like the fact an instance is visible from another one.

(i.e. I don't want Obj::obj1.obj2.obj1 to be a valid syntax)

Is there any workaround or better patterns to avoid such behaviour?

Upvotes: 2

Views: 80

Answers (2)

Bitwize
Bitwize

Reputation: 11220

Public static members (either functions or objects) are always able to be found during lookup from an instance of that class -- so the only way to avoid it is to not offer these as static members from the type you are providing.

Due to your requirement of keeping Obj's constructor private, we need to use friendship somewhere so that an external body can provide these objects and access Obj's constructor without also being of type Obj (which is what allows for the chaining).

For this reason I suggest a secondary class or struct type so that it can be friended to allow it to call Obj's constructors:

class ObjConstants;
class Obj
{
  // Note: friendship here so that ObjConstants can construct it
  friend ObjConstants;
private:
  int _x;
  Obj(int x) : _x() {}
};

class ObjConstants {
public:
  static const Obj obj1;
  static const Obj obj2;
};
const Obj ObjConstants::obj1(1);
const Obj ObjConstants::obj2(2);

int main()
{
  Obj o1 = ObjConstants::obj1;
}

This could also just as easily be a class containing static factory functions:

class ObjConstants {
public:
  static const Obj& getObj1();
  static const Obj& getObj2();
};

or even just a bunch of namespace-scoped factory functions that are each friended individually:

const Obj& getObj1();
const Obj& getObj2();

class Obj {
  friend const Obj& getObj1();
  friend const Obj& getObj2();
  ...
};

When you use an external holder (whether it be type or function) for the static objects, each access of the object returns a different type than the holder (in this case, Obj) which prevents being able to chain obj1.obj2.obj1


It's also worth noting that this approach also works with static constexpr objects. You can't define static constexpr objects in the body of its own class since the class is incomplete at that point, e.g.:

class Foo {
public:
  static inline constexpr auto foo = Foo{}; // Error: 'Foo' is incomplete!  
};

And so using a secondary holder type (whether it be a class or a namespace) makes it possible for the definition to be complete by that point:

class Foo { ... };
class FooConstants {
public:
  static inline constexpr auto foo = Foo{...};
};

Note: In general I'd recommend against static const objects with external linkage like this to avoid static initialization issues

Upvotes: 3

Kevin Anderson
Kevin Anderson

Reputation: 7010

You want a variant of the Factory Pattern here. This is basically a static method that has access to your private constructor, but dictates the terms on what is returned. See below:

class Obj
{
private:
  int _x;
  Obj(int x) : _x(x) {}
// Do the two lines below if you want, you'll catch more compiler errors that way
//  Obj(const Obj&) = delete; // Delete copy constructor
//  Obj& operator=(const Obj&) = delete; // Delete assignment operator

public:
  static Obj& getObj1();
  static Obj& getObj2();
};

Obj& Obj::getObj1()
{
    static Obj obj1{ 1 }; // Only initialized when getObj1() is called, and only once
    return obj1;
}

Obj& Obj::getObj2()
{
    static Obj obj2{ 2 }; // Only initialized when getObj2() is called, and only once
    return obj2;
}

int main()
{
  Obj& ref_o1 = Obj::getObj1();  // Requires a reference
  //Obj notref_o1 = Obj::getObj1();  // Fails to compile, not a reference!
  Obj& ref_o2 = Obj::getObj2();

  Obj& ref_other_o1 = Obj::getObj1(); // References the SAME object as ref_o1
}

You can also make your factory take an argument instead for which instance you want, and hold a static collection of your objects there if you'd like, though you may run into needing to give copy/assign/destruct friend access to the container. But if you want a contained set, what's above should be a good blueprint for you.

My method above of putting the allocation in the static method (not globally) also avoids the Static Initialization Order Fiasco problem that Human-Compiler references.

Upvotes: -1

Related Questions