문제

I have a lot of subclasses of an abstract class Letter, like A, B, C, D, etc. Letter has an integer ID variable, and every subclass of Letter gets assigned a unique id.

I then have another class, call it Alphabet. Alphabet has an

list<shared_ptr<Letter>> 

member. Here is the problem... I would like to elegantly add B's and C's or other subclasses of Letter to particular instances of Alphabets. I think the most convenient way to do this would be to use the integer id of subclass somehow. In other words, I want to be able to have something like Alphabet.addLetter(int id), so if I did alphabet1.add(14), it would somehow add a shared_ptr for class H to the list.

Is there an elegant way to do this, avoiding some huge if statement where that I need to constantly update every time I add or remove one of the B, C, D, E, etc. classes? I'm hoping there's some kind of template-solution, but I'm not very familiar with advanced c++ notions like factories and templates. The naive thing I wanted was some kind of vector/map that converted my ids into class names, so that I could do something like

list.push_back(shared_ptr<classVector(i)>(new classVector(i))

or something like that, though I have no idea if that's possible.

Thanks!

p.s. I just chose the Alphabet example because I didn't want to give unnecessary detail. Obviously I'm not trying to design alphabets in such a silly way, lol.

edit: I'm struggling to make this make sense. My goal is to be able to create new subclasses of Letter very quickly with minimal effort. I would like to avoid having to type out code that looks like...

list.push_back(shared_ptr<X>(...));

every time I make a new letter. Does this make sense at all?

도움이 되었습니까?

해결책 2

This is relatively easy if I understood you correctly, using what's called a factory-pattern.

If you can list all of the derived types:

Letter header:

struct Letter {
    enum LetterEnum {LetterA, LetterB, LetterC, LetterCount};
    
    virtual ~Letter() {} //base types should always have virtual destructor
    virtual void foo() = 0;
  
    static std::unique_ptr<Letter> construct(LetterEnum c);
};

Implementation headers:

struct A : Letter {
  void foo() override;
};

struct B : Letter {
  void foo() override;
};

struct C : Letter {
  void foo() override;
};

Letter body:

std::unique_ptr<Letter> Letter::construct(Letter::LetterEnum c) 
{
    switch(c) {
    case Letter::LetterA : return make_unique<A>();
    case Letter::LetterB : return make_unique<B>();
    case Letter::LetterC : return make_unique<C>();
    default: throw ...;
    }
}

Usage:

int main() {
    char c;
    std::cin >> c;
    //get a letter of the derived type associated with the letter entered
    std::unique_ptr<Letter> ptr = Letter::construct(c);    
}

If you can't list all of the derived types:

Allow the derived types to register themselves with the Letter class, and then Letter can use that to create each of the derived types. This way, adding and deleting the derived types involves no changes to any other files. Easy!

struct Letter {
    virtual ~Letter() {} //destructor is always virtual when inheretence is involved
    ....
    //this is a "shared" function in the Letter class itself
    //it takes a letter, and returns a dynamically allocated instance
    //of the derived type corresponding with that letter
    static std::unique_ptr<Letter> construct(char c);
    //this typedef represents the actual function that returns 
    //each dynamically allocated derived type
    typedef std::function<std::unique_ptr<Letter>()> letter_ctor;
    //this is a "shared" function in the Letter class itself
    //it takes a letter, and a function that creates derived types, 
    //and saves them inside the container ctors
    static bool register(char c, letter_ctor func);
private:
    //this is a "shared" member in the Letter class.  
    //There is only one shared by all of the Letters.  Like a global.
    //When you give it a letter, it gives you a function.
    //and is VERY fast for large numbers of entries
    static std::unordered_set<char,letter_ctor> ctors;
};

and in your implementation file:

//here's the function that derived types register themselves with
//pretty straightforward, just inserts the pair into the unordered_map
bool Letter::register(char c, Letter::letter_ctor func)
{return Letter::ctors.insert(std::make_pair(c,std::move(func))).second;}

//and here's the function that creates the derived types
//it checks if the letter is in the unordered_map
//if the letter isn't there, it throws an exception
//otherwise, it calls the function associated with that letter
//which creates the derived type on the heap, and returns a pointer to it
std::unique_ptr<Letter> Letter::construct(char c) 
{
     auto it = Letter::ctors.find(c);
     if (it == Letter::ctors.end())
         throw ...;
     return it->second(); //construct that letter
}

and then your derived types do this:

//you know this part
struct LetterA : public Letter 
{
   ....
};
//derived types have to register themselves:
//this is a global, so when the program loads, it automatically calls this
//even before main runs*
//it registers the letter 'A' and a function that creates a LetterA class on the heap
static bool registerA = Letter::register('A', [](){return make_unique<LetterA>();});

and then you can easily create arbirary derived types!

int main() {
    char c;
    std::cin >> c;
    //get a letter of the derived type associated with the letter entered
    std::unique_ptr<Letter> ptr = Letter::construct(c);
}

*It doesn't always get called before main. If you have problems, put an bool init_A(); in the A header, and bool init_A(){return true;} in the A implementation file, and in your main file have static bool AInit=init_A(); which should force it. This is almost never needed in practice though.


As a side note, these depend on having a make_unique, which should have been in C++11, but was left out due to oversight. It will be in C++14. In the meantime, use this:

template<class T, class...Us>
std::unique_ptr<T> make_unique(Us&&...us) 
{return std::unique_ptr<T>(new T(std::forward<Us>(us)...));}

다른 팁

This is pretty hard to follow, but I think what you want is something along the lines of the following:

// where make_unique<> is from C++14 in std:: or like:
template <typename T, typename ... TArgs>
std::unique_ptr<T> make_unique(TArgs &&... args) {
  return std::unique_ptr<T>(new T(std::forward<TArgs>(args)...));
}

struct Letter {
  virtual ~Letter() { }
  virtual void foo() = 0;
};
template <unsigned int N> struct LetterCode; // Note: no default implementation!

struct Alphabet {
  // Indexed access, if you'll have 1 of each type max:
  std::vector<std::unique_ptr<Letter>> v;

  // If you don't need parameters, as mentioned in comments below ...
  template <unsigned int N>
  void addLetterN() {
    if (N > v.size() + 1) { v.resize(N + 1); }
    v[N] = make_unique<LetterCode<N>::type>(); // see below ...
  }

  // If your coding is complete from 0...N, this does the whole shebang.
  template <unsigned int N>
  void addLettersN() {
    addLetters<N - 1>();
    addLetterN<N>();
  }
  template <>
  addLettersN<0>() {
    addLetterN<0>();
  }
};

If you need numeric codes for something like deserialization and never need constructor arguments, you can use a type trait template like follows to statically 'register' the types:

struct B : Letter {
  B(int n, bool b, char const *name);
  void foo() override;
};
template <> struct LetterCode<2> { using type = B; };

struct C : Letter {
  C(double d);
  void foo() override;
};
template <> struct LetterCode<3> { using type = C; };

void bar() {
  Alphabet a;
  a.addLetterN<2>();
  a.addLetterN<3>();

  // --OR--
  a.addLettersN<3>(); // will do 0...3 in one fell swoop.

  for (auto &i : a.v) {
    if (!i) { continue; } // v is sparse, unlike l
    i->foo();
}

If you need generalized constructor argument passing, you can use perfect forwarding, which is designed for cases like this and obviates the need for enum IDs, etc., from older styles of factories:

struct Alphabet {
  std::list<std::unique_ptr<Letter>> l;

  // variadic factory that chucks new (shared_ptr) objects in the list.
  template <typename T, typename ... TArgs>
  void addLetter(TArgs && ... args) {
    l.push_back(make_unique<T>(std::forward<TArgs>(args)...));
  }
};

void baz() {
  Alphabet a;
  a.addLetter<B>(1, false, "pony");
  a.addLetter<C>(2.718281828);

  for (auto &i : a.l) {
    i->foo(); // can call virtual funcs here all you want ...
  }
}

My understanding is that you want to create a instance of one of the classes, dependend on an id which relates to the class from which an instance should be created.

If so, please have a look for factory pattern. There are a lot of factory implementations, also based on template recursive expansion of a typelist.

Pseudo Code:

Factory<A,B,C,D> fac; // the list must be changed, if some more classes comes and goes
id_type id;

list<base> l;

l.push_back=fac.Create(id);

It is also quite simple to implement such a class yourself.

The goal is simple: create the function factories that returns an array of Alphabet makers.

The index of the letter and the index into the array will be the same.

Ideally, we want to auto-generate said index without having to manually set it.

#include <memory>
#include <vector>
#include <iostream>

template<class T>using Type=T;

template<class...Ts>struct types:std::integral_constant<unsigned,sizeof...(Ts)>
{typedef types type;};
template<class T,class types>struct index_of;
template<class T,class T0, class...Ts>struct index_of<T,types<T0,Ts...>>:
  std::integral_constant<unsigned,index_of<T,types<Ts...>>::value+1>
{};
template<class T,class...Ts>struct index_of<T,types<T,Ts...>>:
  std::integral_constant<unsigned,0>
{};
template<unsigned,class types>struct type_at;
template<unsigned N, class T,class...Ts>struct type_at<N,types<T,Ts...>>:
  type_at<N-1,types<Ts...>> {};
template<class T,class...Ts>struct type_at<0,types<T,Ts...>>{
  typedef T type;
};
template<unsigned N,class types>
using type_at_t=typename type_at<N,types>::type;

template<template<class>class Target,unsigned N,class types>
struct nth_apply;
template<template<class>class Target,unsigned N,class...Ts>
struct nth_apply<Target,N,types<Ts...>>{
  typedef Target<type_at_t<N,types<Ts...>>> type;
};
template<template<class>class Target,unsigned N,class types>
using nth_apply_t=typename nth_apply<Target,N,types>::type;

This is the type that produces the function pointers for us:

template<class T>struct shared_maker{
  template<class...Args>
  std::shared_ptr<T> operator()(Args&&...args)const{
    return std::make_shared<T>(std::forward<Args>(args)...);
  }
  template<class R, class... Args>
  operator Type<R(Args...)>*() const{
    return [](Args... args)->R{
      return shared_maker{}(std::forward<Args>(args)...);
    };
  }
};

Here is what we do for the actual letter types. We forward declare them:

struct A; struct B; // etc

Stick them into a list of types:

typedef types<A,B> Alphabet_Types;

Now, our simple test Alphabet type:

struct Alphabet {
  virtual unsigned get_index() const = 0;
};

And a CRTP helper that gets the index of the letter from its offset into the list of types! The virtual get_indexes is just for debugging:

template<class D>
struct Letter:Alphabet{
  static const unsigned index = index_of<D, Alphabet_Types>::value;
  virtual unsigned get_index() const override { return index; }
};

Now the signature of our array-producer:

typedef std::shared_ptr<Alphabet> spAlphabet;
std::array<spAlphabet(*)(), Alphabet_Types::value> factories();

Here is how we define our (toy) letter classes:

struct A:Letter<A>{};
struct B:Letter<B>{};

ie, use Letter<> as a CRTP base instead of Alphabet.

The only thing left is to write the function factories.

Index boilerplate. C++1y has a replacement:

template<unsigned...>struct indexes{typedef indexes type;};
template<unsigned Max, unsigned... Is> struct make_indexes:make_indexes<Max-1,Max-1,Is...>{};
template<unsigned...Is>struct make_indexes<0,Is...>:indexes<Is...>{};

The actual implementation via a helper function. We get a pack of indexes and expand it, building our std::array of function pointers from our shared_maker above instantiated with an indexed type from the Alphabet_Types we wrote above:

template<unsigned...Is>
std::array<spAlphabet(*)(), Alphabet_Types::value> factories(indexes<Is...>){
  return {nth_apply_t<shared_maker,Is,Alphabet_Types>{}...};
}

The actual factories function just forwards to the above helper:

std::array<spAlphabet(*)(), Alphabet_Types::value> factories(){
  return factories(make_indexes<Alphabet_Types::value>{});
}

And some trivial test code:

int main() {
  std::vector<spAlphabet> vec;
  auto builders = factories();
  for (int i = 0; i < 2; ++i) {
    vec.push_back(builders[i]());
  }
  for( auto&& ptr:vec ) {
    std::cout << ptr->get_index() << "\n";
  }
}
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top