Pergunta

Qual é um bom padrão de classe/design existente para construção/inicialização de vários estágios de um objeto no C ++?

Eu tenho uma aula com alguns membros de dados que devem ser inicializados em pontos diferentes no fluxo do programa, para que a inicialização seja adiada. Por exemplo, um argumento pode ser lido de um arquivo e outro da rede.

Atualmente, estou usando o Boost :: Opcional para a construção atrasada dos membros de dados, mas está me incomodando que opcional seja semanticamente diferente do atraso construído.

O que eu preciso lembra os recursos do aplicativo de função parcial do Boost :: Bind e Lambda e, usando essas bibliotecas, provavelmente posso projetar a construção em vários estágios - mas prefiro usar classes testadas existentes. (Ou talvez haja outro padrão de construção em vários estágios com o qual não conheço).

Foi útil?

Solução

A questão principal é se você deve ou não distinguir objetos completamente povoados de objetos incompletamente povoados no nível do tipo. Se você decidir não fazer uma distinção, basta usar boost::optional Ou semelhante ao que você está fazendo: isso facilita obter a codificação rapidamente. OTOH Você não pode fazer com que o compilador aplique o requisito de que uma função específica requer um objeto completamente povoado; Você precisa executar a verificação do tempo de execução de campos a cada vez.

Tipos de grupo de parâmetros

Se você distinguir objetos completamente povoados de objetos incompletamente povoados no nível do tipo, poderá fazer cumprir o requisito de que uma função seja passada um objeto completo. Para fazer isso, sugiro criar um tipo correspondente XParams Para cada tipo relevante X. XParams tem boost::optional Membros e funções do setter para cada parâmetro que podem ser definidos após a construção inicial. Então você pode forçar X ter apenas um construtor (não cópia), que leva um XParams como seu único argumento e verifica se cada parâmetro necessário foi definido dentro disso XParams objeto. (Não tenho certeza se esse padrão tem um nome - alguém gosta de editar isso para nos preencher?)

Tipos de "objeto parcial"

Isso funciona maravilhosamente se você realmente não precisa Faz Qualquer coisa com o objeto antes de ser completamente povoada (talvez além de coisas triviais, como recuperar os valores do campo). Se você precisa tratar às vezes um incompletamente povoado X Como um "cheio" X, em vez disso, você pode fazer X derivar de um tipo XPartial, que contém toda a lógica, mais protected Métodos virtuais para realizar testes de pré -condição que testam se todos os campos necessários são preenchidos. Então se X Garante que ele só possa ser construído em um estado completamente povoado, pode substituir os métodos protegidos com verificações triviais que sempre retornam true:

class XPartial {
    optional<string> name_;

public:
    void setName(string x) { name_.reset(x); }  // Can add getters and/or ctors
    string makeGreeting(string title) {
        if (checkMakeGreeting_()) {             // Is it safe?
            return string("Hello, ") + title + " " + *name_;
        } else {
            throw domain_error("ZOINKS");       // Or similar
        }
    }
    bool isComplete() const { return checkMakeGreeting_(); }  // All tests here

protected:
    virtual bool checkMakeGreeting_() const { return name_; }   // Populated?
};

class X : public XPartial {
    X();     // Forbid default-construction; or, you could supply a "full" ctor

public:
    explicit X(XPartial const& x) : XPartial(x) {  // Avoid implicit conversion
        if (!x.isComplete()) throw domain_error("ZOINKS");
    }

    X& operator=(XPartial const& x) {
        if (!x.isComplete()) throw domain_error("ZOINKS");
        return static_cast<X&>(XPartial::operator=(x));
    }

protected:
    virtual bool checkMakeGreeting_() { return true; }   // No checking needed!
};

Embora possa parecer a herança aqui é "de volta à frente", fazer dessa maneira significa que um X pode ser fornecido com segurança em qualquer lugar e XPartial& é solicitado, então essa abordagem obedece ao Princípio de substituição de Liskov. Isso significa que uma função pode usar um tipo de parâmetro de X& para indicar que precisa de um completo X objeto, ou XPartial& para indicar que pode lidar com objetos parcialmente povoados - nesse caso, seja um XPartial objeto ou um completo X pode ser passado.

Originalmente eu tinha isComplete() Como protected, mas descobri que isso não funcionou desde XA cópia do ctor e o operador de atribuição deve chamar essa função em seus XPartial& argumento, e eles não têm acesso suficiente. Na reflexão, faz mais sentido expor publicamente essa funcionalidade.

Outras dicas

Devo estar perdendo alguma coisa aqui - faço esse tipo de coisa o tempo todo. É muito comum ter objetos grandes e/ou não necessários por uma classe em todas as circunstâncias. Então, crie -os dinamicamente!

struct Big {
    char a[1000000];
};

class A {
  public: 
    A() : big(0) {}
   ~A() { delete big; }

   void f() {
      makebig();
      big->a[42] = 66;
   }
  private:
    Big * big;
    void makebig() {
      if ( ! big ) {
         big = new Big;
      }
    }
};

Não vejo a necessidade de nada mais sofisticado do que isso, exceto que Makebig () provavelmente deve ser const (e talvez embutido), e o grande ponteiro provavelmente deve ser mutável. E, é claro, A deve ser capaz de construir Big, o que pode, em outros casos, significar o cache dos parâmetros do construtor da classe contida. Você também precisará decidir sobre uma política de cópia/atribuição - provavelmente eu proibiria ambos para esse tipo de classe.

Não conheço nenhum padrão para lidar com esse problema específico. É uma pergunta complicada de design e um tanto exclusiva para idiomas como C ++. Outra questão é que a resposta a esta pergunta está intimamente ligada ao seu estilo de codificação individual (ou corporativo).

Eu usaria ponteiros para esses membros e, quando eles precisam ser construídos, alocassem -os ao mesmo tempo. Você pode usar o Auto_PTR para estes e verificar contra o NULL para ver se eles são inicializados. (Acho que os ponteiros são um tipo "opcional" interno em C/C ++/Java, existem outros idiomas em que o NULL não é um ponteiro válido).

Uma questão de estilo é que você pode confiar em seus construtores para fazer muito trabalho. Quando estou codificando OO, tenho os construtores que fazem trabalho suficiente para obter o objeto em um estado consistente. Por exemplo, se eu tiver um Image Classe e eu quero ler de um arquivo, eu poderia fazer isso:

image = new Image("unicorn.jpeg"); /* I'm not fond of this style */

ou, eu poderia fazer isso:

image = new Image(); /* I like this better */
image->read("unicorn.jpeg");

Pode ser difícil raciocinar sobre como um programa C ++ funciona se os construtores tiverem muito código neles, especialmente se você fizer a pergunta: "O que acontece se um construtor falhar?" Este é o principal benefício de mover o código dos construtores.

Eu teria mais a dizer, mas não sei o que você está tentando fazer com a construção atrasada.

EDIT: Lembrei -me de que existe uma maneira (um tanto perversa) de chamar um construtor em um objeto em qualquer momento arbitrário. Aqui está um exemplo:

class Counter {
public:
    Counter(int &cref) : c(cref) { }
    void incr(int x) { c += x; }
private:
    int &c;
};

void dontTryThisAtHome() {
    int i = 0, j = 0;
    Counter c(i);       // Call constructor first time on c
    c.incr(5);          // now i = 5
    new(&c) Counter(j); // Call the constructor AGAIN on c
    c.incr(3);          // now j = 3
}

Observe que fazer algo tão imprudente quanto isso pode ganhar o desprezo de seus colegas programadores, a menos que você tenha razões sólidas para usar essa técnica. Isso também não atrasa o construtor, apenas permite chamá -lo novamente mais tarde.

Usar Boost.Optional parece uma boa solução para alguns casos de uso. Não joguei muito com isso, então não posso comentar muito. Uma coisa que lembro ao lidar com essa funcionalidade é se posso usar construtores sobrecarregados em vez de construtores de inadimplência e copiar.

Quando eu preciso de essa funcionalidade, eu apenas usava um ponteiro para o tipo do campo necessário como este:

public:
  MyClass() : field_(0) { } // constructor, additional initializers and code omitted
  ~MyClass() {
    if (field_)
      delete field_; // free the constructed object only if initialized
  }
  ...
private:
  ...
  field_type* field_;

Em seguida, em vez de usar o ponteiro, acessaria o campo através do seguinte método:

private:
  ...
  field_type& field() {
    if (!field_)
      field_ = new field_type(...);
    return field_;
  }

Eu omiti semântica de acesso const

A maneira mais fácil que eu conheço é semelhante à técnica sugerida pela Dietrich EPP, exceto que permite atrasar verdadeiramente a construção de um objeto até um momento de sua escolha.

Basicamente: reserve o objeto usando MALLOC em vez de novo (ignorando o construtor) e depois chame o novo operador sobrecarregado quando você verdadeiramente deseja construir o objeto por meio de colocação nova.

Exemplo:

Object *x = (Object *) malloc(sizeof(Object));
//Use the object member items here. Be careful: no constructors have been called!
//This means you can assign values to ints, structs, etc... but nested objects can wreak havoc!

//Now we want to call the constructor of the object
new(x) Object(params);

//However, you must remember to also manually call the destructor!
x.~Object();
free(x);

//Note: if you're the malloc and new calls in your development stack 
//store in the same heap, you can just call delete(x) instead of the 
//destructor followed by free, but the above is the  correct way of 
//doing it

Pessoalmente, a única vez em que usei essa sintaxe foi quando tive que usar um alocador baseado em C personalizado para objetos C ++. Como Dietrich sugere, você deve questionar se realmente deve atrasar a chamada do construtor. o base O construtor deve executar o mínimo nu para colocar seu objeto em um estado reparável, enquanto outros construtores sobrecarregados podem executar mais trabalho conforme necessário.

Não sei se há um padrão formal para isso. Em lugares onde eu o vi, chamamos de "preguiçoso", "demanda" ou "sob demanda".

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top