Como eu posso fazer uma função receber somente objetos alocados com new? [RESOLVIDO]

1. Como eu posso fazer uma função receber somente objetos alocados com new? [RESOLVIDO]

Samuel Leonardo
SamL

(usa XUbuntu)

Enviado em 16/10/2018 - 22:21h




  


2. MELHOR RESPOSTA

Paulo
paulo1205

(usa Ubuntu)

Enviado em 18/10/2018 - 10:21h

Nesse caso você terá de fazer adaptações.

Se Entity será uma classe base, o primeiro passo é definir bem a interface que será utilizada por todos os seus descendentes, por meio de funções-membros virtuais. Em particular, o destrutor tem de ser virtual.

Um tipo de função-membro que pode ser útil, dependendo de como você modelar suas classes, é o que se conhece como construtor virtual, que é uma função virtual sobreposta em cada classe descendente que constrói um objeto novo do seu próprio tipo e retorna um ponteiro para o objeto recém-construído. Como os ponteiros para objetos da classe derivada podem ser atribuídos a ponteiros para a classe base, esses ponteiros podem ser armazenados no container, e o emprego de funções virtuais garante que cada objeto vai executar as funções corretas da sua própria classe.

Há dois tipos principais de construtores virtuais, os de duplicação de objetos já existentes, que invocam construtores de cópia para copiar a si próprios, e os de geração de semelhantes, que invocam um construtor de objetos de seu próprio tipo, mas com outros argumentos.

A classe Entity poderia então ser alterada para se parecer com o seguinte.

class Entity {
protected:
/* Membros de dados e funções que sejam comuns a TODAS as classes derivadas. */

public:
/* Não haverá construtores públicos, pois a classe é abstrata, e não permite criar objetos diretamente. */

// O destrutor tem de ser virtual.
virtual ~Entity(){ }

/* Construtores virtuais. */
virtual Entity *clone() const = 0; // Pode ser const, pois não altera o objeto usado para chamá-lo.
virtual Entity *move() = 0; // Usa um construtor de movimentação para mover este objeto para o novo,
// colocando em seu lugar conteúdo neutro para ser destruído.
virtual Entity *make_new(void *arg) const = 0; // Cada classe derivada interpreta arg a seu modo.
// etc.

/* Outras funções da interface pública (exemplos). */
void print() const = 0;
void save_to_disk() const = 0;
// etc.
};


Na classe Elements, as versões da função-membro add() que derreferenciavam os ponteiros para invocar construtores de cópia agora terão de invocar os construtores virtuais.

class Elements {
/* ... */

public:
/* ... */
void add(const Entity *p){ entities.emplace_back(p->clone()); }
void add(Entity *p){ add(const_cast<const Entity *>(p)); }

void add(const pEntity &e){ entities.emplace_back(e->clone()); }
void add(pEntity &&e){ entities.emplace_back(std::move(e)); }
void add(pEntity &e){ entities,emplace_back(e); }

/* ... */
};


A primeira versão de add() da postagem anterior, que encaminhava argumentos diretamente a um construtor de Entity, não mais seria válida, até porque objetos da classe abstrata Entity não poderiam mais ser construídos diretamente. No entanto, pode-se criar novas versões, tanto baseadas em templates quanto que usem construtores virtuais.

class Elements {
/* ... */

public:
/* ... */
// A função abaixo tem de ser invocada na forma
// “add<ClasseDerivada>(lista_de_argumentos_do_construtor_da_ClasseDerivada)”.
template <class DerivEntity, class... ArgTypes> void add(ArgTypes&&... args){
entities.emplace_back(new DerivEntity(std::forward<ArgTypes>(args)...));
}

// Para objetos preexistentes que não sejam passados como ponteiros, podemos
// usar passagem por referência de Entity, sem necessidade de templates.
void add(const Entity &e){ entities.emplace_back(e.clone()); }
void add(Entity &&e){ entities.emplace_back(e.move()); }
void add(const Entity &e, void *arg){ entities.emplace_back(e.make_new(arg)); }

/* ... */
};


Pelo bloco acima, sendo elems um objeto da classe Elements, “elems.add<DerivEntity>()” produziria o mesmo resultado que “elems.add(DerivEntity())”. Contudo a primeira versão tenderá a ser mais eficiente, pois chama diretamente o construtor da classe derivada e atribui o novo objeto no container, ao passo que a segunda opção cria um objeto como rvalue e depois invoca um dos construtores virtuais desse objeto temporário para mover seus dados para outro objeto, e ainda chama um destrutor sobre o objeto temporário inicial.

De modo semelhante, “elems.add(SomeEntity(), &std::make_tuple(lista, de, argumentos))” poderia ser usado como alternativa a “elems.add<SomeEntity>(lista, de, argumentos)”, só que eu acho que raramente se vai preferir aquele a este.

NOTA: Eu falei à beça, mas não testei nada do que vai acima. Pode ser que haja erros. Use com moderação.

3. Re: Como eu posso fazer uma função receber somente objetos alocados com new? [RESOLVIDO]

Fernando
phoemur

(usa Debian)

Enviado em 16/10/2018 - 22:54h

O padrão do C++ não oferece nenhuma maneira legal de determinar se um ponteiro é válido ou não, nem de saber se este ponteiro precisa ser liberado com delete/delete[] ou é apenas uma referência a um tipo no Stack...

Ou seja: Use smart pointers!!! (std::unique_ptr, std::shared_ptr)

Tem muitas coisas que podem dar errado ao alocar manualmente:
Derreferenciar um ponteiro nulo.
Derreferenciar um ponteiro que já foi liberado com delete.
Derreferenciar um ponteiro inválido cujo objeto apontado não mais existe.
Deletar 2 vezes -> undefined behavior
Não deletar e vazar memória.
Se houver exceções a coisa complica mais, e a chance dos erros acima é ainda maior pois é mais difícil prever o caminho que o programa vai tomar...

Use smart pointers!!!

______________________
https://github.com/phoemur


4. Re: Como eu posso fazer uma função receber somente objetos alocados com new? [RESOLVIDO]

Paulo
paulo1205

(usa Ubuntu)

Enviado em 16/10/2018 - 22:59h

Troque a função-nembro add() por uma função emplace(), que receba os argumentos que os construtores do objeto receberiam (podendo usar inclusive parameter-packing), e construa o objeto na memória nessa hora.

Opcionalmente, você pode ter funções que copiem ou movam objetos já construídos para outro que você vai construir em memória com o mesmo valor.


5. Re: Como eu posso fazer uma função receber somente objetos alocados com new? [RESOLVIDO]

Paulo
paulo1205

(usa Ubuntu)

Enviado em 16/10/2018 - 23:39h

Eu concordo com o Phoenix que smart pointers ajudam, mas eles, sozinhos, podem ter a mesma dificuldade que a experimentada originalmente pelo SamL, de não ter garantia absoluta de que o ponteiro passado como argumento ao construtor será de fato um ponteiro alocado dinamicamente.

Uma coisa (OK) é fazer
std::unique_ptr<int> a{new int(5)}; 

e outra (sintaticamente válida, porém errônea, com possivelmente graves consequências) seria ter o seguinte.
int i{5};
std::unique_ptr<int> a{&i};


Creio que smart pointers poderiam ser usados pelo SamL dentro da classe que vai envelopar os objetos. Contudo, ainda acho que a melhor maneira de controlar onde tais objetos serão alocados é tendo o controle sobre sua construção.


6. Re: Como eu posso fazer uma função receber somente objetos alocados com new?

Paulo
paulo1205

(usa Ubuntu)

Enviado em 17/10/2018 - 11:11h

SamL escreveu:

Paulo, eu imagino que ter uma função que constrói o objeto dentro dela recebendo os parametros do construtor seja um pouco trabalhosa, dado o fato que preciso fazer uma versão da função pra cada objeto criado. Mas, vou ver aqui o que você quis dizer com parameter-packing, isso é novo pra mim. Ah sim, eu to usando C++11, então acho que deve suportar algumas features a mais dessa versão.


Não é difícil. Se você já usou algum container da STL com C++11 em diante, já deve ter usado, por exemplo, std::vector::emplace_back(), std::map::emplace() etc, que são versões de operações de inserção que constrõem os objetos diretamente dentro dos conatiners, em vez de copiar objetos previamente construídos.

Eis um exemplo de implementação.

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


/*
Suponho que Entity é um tipo não-trivial, que precisa de gestão de recursos
(aqui mostrado através de um ponteiro muito vagabundo).
*/
class Entity {
private:
int x, *y;

public:
// Construtor default.
Entity(): x(0), y(nullptr) { }

// Construtor de cópia.
Entity(const Entity &other):
x(other.x), y(other.y? new int(*other.y): nullptr)
{ }

// Construtor de movimentação.
Entity(Entity &&older): x(older.x), y(older.y) { older.y=nullptr; }

/* Outros possíveis construtores. Alguns exemplos dados abaixo. */
Entity(int i): x(i), y(new int(i)) { }
Entity(int i, int j): x(i), y(new int(j)) { }

~Entity(){ if(y) delete y; }

/* Etc. */

void print() const {
std::cout << "x=" << x << ", y=" << y;
if(y)
std::cout << " (*y=" << *y << ')';
std::cout << ", this=" << this << ".\n";
}
};

typedef std::shared_ptr<Entity> pEntity;

class Elements {
private:
std::vector<pEntity> entities; // Um container de smart pointers.

public:
Elements(){ /* ... */ }
Elements(const Elements &other){ /* ... */ }
Elements(Elements &&older){ /* ... */ }

/*
Constrói um novo objeto Entity (1), encaminhando ao seu construtor
os mesmos argumentos recebidos pela função-membro, e constrói um
novo objeto no final do vetor de smart pointers, que recebe o
ponteiro para o Entity recém-criado (2).

Isto funciona para invocar qualquer um dos construtores válidos da
classe Entity.
*/
template <class... ArgTypes> void add (ArgTypes&&... args){
entities.emplace_back(
new Entity(std::forward<ArgTypes>(args)...) // 1
); // 2
}

/*
Se receber um pEntity, não constrói objeto novo, mas usa apenas
o smart pointer.
*/
void add(pEntity &preexisting){ entities.emplace_back(preexisting); }
void add(const pEntity &preexisting){ entities.emplace_back(preexisting); }
void add(pEntity &&preexisting){ entities.emplace_back(std::move(preexisting)); }

/*
Se receber um *Entity, assume que o ponteiro não está regido por
um smart pointer, e faz uma cópia do objeto apontado por ele.
Isso é feito chamando a primeira versão de add(), que vai acabar
invocando o construtor de cópia da classe Entity.
*/
void add(const Entity *p){ add(*p); }
void add(Entity *p){ add(*p); }


void print(const char *name) const {
for(size_t k=0; k<entities.size(); k++){
std::cout << name << '[' << k << "]: ";
entities[k]->print();
}
}
};


int main(){
Elements elems;
Entity e1, e2{1};
Entity *pe1=new Entity(2);
pEntity pe2{new Entity(3, 4)}, pe3{new Entity(5)};

elems.add(); // 1ª versão de add(): chama construtor default de Entity.
elems.add(e1); // 1ª versão de add(): chama construtor de cópia de Entity.
elems.add(std::move(e2)); // 1ª versão de add(): chama construtor de movimentação de Entity (por força de std::move()).
elems.add(pe1); // 6ª versão de add(), que chama a 1ª, que chama construtor de cópia de Entity.
elems.add(Entity(6, 7)); // 1ª versão de add(): chama construtor de movimentação de Entity.
elems.add(8, 9); // 1ª versão de add(): chama construtor específico de Entity.
elems.add(pe2); // 3ª versão de add(): o 5º elemento aponta para exatamente o mesmo objeto que pe2.
elems.add(std::move(*pe3)); // 1ª versão de add(): chama construtor de movimentação de Entity (por força de std::move()).

std::cout << "e1: "; e1.print();
std::cout << "e2: "; e2.print(); // Aqui, y aparecerá zerado pois foi movido para elems.
std::cout << "*pe1: "; pe1->print();
std::cout << "*pe2: "; pe2->print();
std::cout << "*pe3: "; pe3->print(); // Aqui, y aparecerá zerado pois foi movido para elems.
std::cout << '\n';
elems.print("elems");

delete pe1; // Para deixar o valgrind feliz. :)
}







Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts