Projetando em torno de constness superficial com herança

5

Antecedentes

Estou escrevendo uma aula de manipulação de imagens. Para esta questão, dois requisitos da classe são de interesse:

  • Deve ter exatidão const "profunda".
  • Deve permitir aliases de subimagem, seções a.k.a ou fatiamento sem executar cópias profundas de dados de imagem.

Por constância profunda quero dizer o seguinte. Considere esta classe de exemplo:

class Image{
public:
    ...
    Image(const Image&);

    Image alias(int x0, int y0, int x1, int y1) const;
private:
    Buffer image_data;
};

E suponha que você tenha uma função que tenha passado por uma referência const para uma imagem, da seguinte forma:

void foo(const Image& im){
    Image alias = im.alias(0,0, 10, 10);
    // Hold on, the mutable alias can modify the data in the const argument!
}

A chamada de função alias acima derruba a constância do argumento porque o conteúdo da imagem do argumento pode ser modificado através do alias não-constante. Eu não posso retornar um const Image de alias porque bem, isso não faz sentido realmente e o construtor de cópia de Image construirá uma instância mutável de qualquer maneira.

Minha solução (e eu a vi usada com frequência, então não reivindico originalidade aqui) é introduzir duas classes (interfaces realmente, mas pela simplicidade elas são classes neste exemplo e retorno por valor é ok, em realidade alias(...) retorna um shared_ptr para a interface):

class ConstImage{
public:
    ConstImage alias(...) const;
};

class Image{
public:
    Image alias(...);
};

Agora, o seguinte código:

void foo(const Image& im){ 
    // compile error, cannot convert ConstImage to Image, perfect!
    Image alias = im.alias(...); 
}

E é claro que você mudaria a assinatura para void foo(const ConstImage& im) para aceitar invocações como foo(const_im.alias(...)) ou outros ConstImage de objetos.

Agora, é claro, queremos que foo(const ConstImage& im) possa ser chamado (1) com um Image mutável e aqui é onde a pergunta entra.

Pergunta

Usando herança: class Image : public ConstImage fará todos os Image tipos utilizáveis por meio de ConstImage referências e ponteiros. Ele também facilitará a reutilização de código entre as implementações const e non-const que, por necessidade, são muito similares.

Isso resolve o problema mencionado acima (1). No entanto, se você olhar para a relação de herança como uma declaração "é uma", então "uma imagem mutável é uma imagem imutável" não faz sentido. Que fala contra o uso de herança aqui.

Minha pergunta é: esse tipo de (ab) uso de herança é amplamente aceitável? Existe alguma outra solução melhor que eu tenha esquecido? Observe que a classe Image é mais de uma interface, portanto, a conversão de valor ingênua de Image para ConstImage não é possível. Embora um poderia provavelmente chegar a algo louco para permitir isso.

    
por Emily L. 01.10.2015 / 16:21
fonte

8 respostas

2

Is there some other better solution that I have overlooked

Um Image "é um" ConstImage no sentido de que sempre pode ser "tratado como const". Usar herança estritamente para modelar relacionamentos "IS A" é também conhecido como "Princípio de Substituição de Liskov" (LSP), que é sobre comportamento. O LSP é cumprido quando uma classe herdada pode sempre substituir sua classe base sem violar a exatidão do programa. E contanto que você não chame um método não-const de Image (o que você normalmente não fará ao passar por uma referência ConstImage ), um Image sempre se comportará como ConstImage .

    
por 01.10.2015 / 16:31
fonte
2

Crie uma classe abstrata ReadableImage . Derive MutableImage e ImmutableImage (e também ImageSlice ) dessa classe. Então o relacionamento é um relacionamento. Não use os parâmetros ImmutableImage em funções, a menos que a função realmente espere que a imagem seja realmente imutável, por exemplo, porque espera que ela não seja modificada por outro thread.

A parte chata disso é o fatiamento, mas esse é um problema que você sempre enfrenta com hierarquias de classe que representam dados em C ++.

    
por 01.10.2015 / 23:31
fonte
2

Se você aceitar que Const apenas significa "não será manipulado daqui", como a maioria de sua pergunta parece implicar, uma const_image não é imutável.

Parece que sua classe é um ponteiro inteligente com acessores extras e manipuladores (se não const).

Você pode se beneficiar baseando-se em std::shared_ptr .

Em seguida, para emular deep-const, sua Image -class precisa fazer o seguinte:

  1. Garanta que uma instância const não tenha manipuladores.

    Isso é feito com facilidade, apenas certifique-se de que nenhum método manipulador seja const -qualificado e todos os métodos do inspetor sejam.

    int getX() const; // inspector
    int setX();       // manipulator
    
  2. Assegure-se de que uma instância const não exponha nenhum estado interno const não profundo.

    Qualquer inspector-method só retorna referências const-qualified ou ponteiros equivalentes, e somente para o estado que emula deep-const, ou não possui nenhum estado vinculado.
    Naturalmente, as cópias profundas podem sempre ser retornadas. (Cópias profundas e cópias rasas são trivialmente equivalentes para estado sem links internos.)

  3. Assegure-se de que uma instância não-const não possa ser construída a partir de uma instância const.

    Agora, este último ponto parece um back-breaker, já que o C ++ infelizmente não permite criar um construtor const .
    Felizmente, podemos contornar isso com mais código: Apenas declare uma segunda classe.

struct image;
struct const_image {
    // ctors, dtor, const inspectors
    // no manipulators, and only expose as const_image, other deep-const type or deep-copy
private:
    friend class image;
    // explicitly defaulted (move-)assignment-operators
    // Whatever members are neccessary to maintain the data.
    // Probably only a single shared_ptr and some indices
};

struct image : const_image {
    // delegating ctors for all but copy-construction and move-constructor
    // copy-ctor accepting a non-const image, probably defaulted
    // move-constructor, (move-)assignment-operators
    // additional non-const inspectors (where possible a thin mask over the const ones)
    // these all return Image or manipulatable internal state
    // manipulators
private:
    // if neccessary, optional additional state for supporting manipulation-operations
};
    
por 06.10.2015 / 02:29
fonte
1

TL; DR

  1. (ideia pessoal, não comprovada)
    • "Este cptr é const , esse mptr não é
  2. Idéia quase comprovada, além do design Mat_<T> do OpenCV
    • Asse a constância no parâmetro de modelo T e
    • Certifique-se de que todos os furos estejam conectados, fazendo algumas alterações no OpenCV.

É difícil, e mesmo bibliotecas de imagens conhecidas, como o OpenCV, não encontram uma solução perfeita. Em vez disso, a maioria das pessoas viveria com o pragmatismo, ou seja, estaria satisfeita com o que quer que tenha conseguido.

Aqui está a minha ideia não comprovada:

  • Em cada classe Image (e Buffer class também), mantenha dois campos de ponteiro brutos:
    • Um é um ponteiro não-const void (ou uchar ).
    • Um é um ponteiro const void (ou uchar ). (Significa que não permite modificar os dados que estão sendo apontados, através deste ponteiro.)
  • Em Image instâncias que precisam "reforçar a constância", mesmo que alguém tenha conseguido obter uma referência não-qualificada a esse Image , o ponteiro não const é intencionalmente configurado como nullptr .
    • Antes de qualquer modificação de pixel, alguém terá que validar o ponteiro antes de prosseguir. Normalmente, se alguém pretende modificar um monte de pixels, um só precisa validar o ponteiro uma vez, portanto, com design adequado, isso não deve se tornar uma sobrecarga de desempenho.
    • Todas as modificações serão feitas através do ponteiro não const e todas as operações somente leitura serão feitas através do ponteiro const.
    • Para imagens mutáveis (ou imagens que permitem modificações), os dois ponteiros sempre terão o mesmo valor.

Esta é apenas uma ideia selvagem e não comprovada. Sinta-se livre para explorar e criticar. Dito isto, a minha ideia é fabricada a partir do uso prolongado do OpenCV e de implementações de processamento de imagem de baixo nível.

Como um bônus, se você já estiver familiarizado com o OpenCV, você pode tentar as duas declarações a seguir:

const auto sz = cv::Size(3, 3); const uchar fillval = 0U; cv::Mat_<uchar> matMutable1B(sz, fillval); cv::Mat_<const uchar> matImmutable1B(sz, fillval);

Observe que o primeiro permite modificações através do estilo usual do OpenCV, por exemplo mat.at(row, col) = fillval; , mat(row, col) = fillval; , mat.ptr(row)[col] = fillval; , enquanto o segundo não permite.

Isso ocorre porque o parâmetro de modelo <T> está qualificado pela segunda declaração.

No entanto, atualmente, isso não é infalível, porque alguém que obtiver o ponteiro via mat.data obterá um uchar* , independentemente da qualificação const do parâmetro de modelo <T> .

Uma distinção muito importante sobre immutability / object state access control versus C++ style notational const-qualification

C ++ const-qualificação afeta código que "vem por / obter" uma referência a uma determinada classe. Assim, ele passa pela const-qualificação por algo como uma cadeia de custódia de tipos. O Image em si não pode descobrir se alguém possuir uma referência a ele e fazer uma chamada para ele é const-qualified ou não. Em vez disso, o C ++ aplica isso ao verificar o código do chamador e bloqueia a tentativa do chamador. A classe Image nunca sabe.

Minha idéia de usar dois ponteiros (const / non-const) e definir o non-const como nullptr é uma tentativa de resolver esse problema em tempo de execução. Assim, alguém que obtiver um Image que não seja classificado como const, mas seu ponteiro de dados não const for nullptr, enfrentará conseqüências de tempo de execução.

Em última análise, se isso for muito difícil de discutir, talvez gaste algum tempo com o OpenCV e tente tanto const-qualify a matriz em si, quanto o parâmetro template, para ver qual técnica atende ao máximo de suas necessidades.

Outra lição aprendida com o OpenCV é que você deve implementar seu próprio mecanismo de contagem de referências na classe image. Não fazer isso torna o código do usuário da biblioteca muito frágil.

    
por 02.10.2015 / 07:47
fonte
1

Parece que estou um pouco atrasado para a festa, mas espero que o OP veja essa resposta. A melhor solução para o seu problema é não usar herança, mas deixar a classe Image como está. Em vez disso, o que você precisa são duas classes Alias: Alias e ConstAlias. Em seguida, altere todas as suas funções que realmente não precisam da própria imagem, mas um alias para usar Alias ou ConstAlias conforme apropriado. O método alias possui uma versão const e non-const, que retorna os tipos ConstAlias e Alias, respectivamente.

Isso soa familiar em algum lugar? Substitua Image por vector e Alias por iterador, e você tem ... bem, metade do STL.

É melhor distinguir explicitamente entre Imagens e Aliases usando o sistema de tipos, o primeiro é um proprietário e o segundo não. A maioria das funcionalidades requer apenas uma visão (possivelmente mutável) dos dados, não precisa realmente mexer com a propriedade. Isso faz com que você pense mais sobre se sua função deve ter uma imagem ou apenas um alias. Ele também permite que você evite o uso de shared_ptr e mantenha um modelo de propriedade muito mais simples e claro (e possivelmente mais eficiente).

    
por 12.10.2015 / 05:08
fonte
0

Acho que seria melhor abordar o problema de uma perspectiva ligeiramente diferente.

O C ++ funciona de várias maneiras com tipos que funcionam como tipos de valor. Ou seja, eles se comportam como tipos internos ou a maioria dos tipos de STL. Você poderia fazer uma classe Image que age como um std::vector e é um tipo de valor com exatidão const 'profunda' simplesmente copiando dados sempre que você copia uma imagem ou tira uma fatia de imagem, mas isso seria um pouco ineficiente devido a copiando. Essas são as semânticas que você gostaria idealmente da sua classe Image .

Você deve pensar no compartilhamento de dados de imagem para eficiência como puramente um detalhe de implementação de sua classe. Não é algo que o usuário final precise saber do ponto de vista da interface. Uma maneira de conseguir isso é usar o copy-on-write para dar a aparência de um tipo de valor, enquanto você está compartilhando dados sempre que possível. Isso costumava ser uma tática de implementação comum para std::string , embora não seja mais, principalmente devido a preocupações de segurança de encadeamento (que você também precisará resolver se quiser usar sua classe Image em cenários de vários encadeamentos) .

Uma implicação dessa abordagem alternativa é que alias() é um nome ruim para sua função de fatia de imagem. Ele expõe um detalhe de implementação no nome de uma API pública. Essa função deve ser chamada slice() . Com uma implementação de copy-on-write, você pode chamar slice() em const Image e obter Image . Nos bastidores, os dados da imagem seriam compartilhados até que você tentasse escrever nela, quando você faria uma cópia. Uma implementação mais sofisticada pode marcar a imagem e copiar somente os blocos que foram tocados pela gravação, portanto, alterar alguns pixels não levaria uma cópia da fatia inteira da imagem.

    
por 05.10.2015 / 19:08
fonte
0

This solves the above mentioned problem (1). However if you look at the inheritance relation as an "is a" statement then "a mutable image IS AN immutable image" makes no sense. Which speaks against using inheritance here.

Bem ... uma imagem mutável IS uma imagem imutável, com recursos extras (para mutação) - o que faz muito sentido (assim como um cirurgião é um médico com capacidades extras). O fato de as capacidades extras serem uma distinção entre mutável e imutável faz pouca diferença (ainda é uma imagem imutável com comportamento mutável adicionado a ela).

Eu imagino que a confusão que criou a questão, é o fato de que na linguagem falada, não faz sentido dizer "um X mutável é um X imutável" (porque no pensamento racional, A ou não-A, mas não ambos).

Esse relacionamento OO "é-um", no entanto, não é exatamente o mesmo relacionamento "é-um" que você fala em inglês.

    
por 06.10.2015 / 11:41
fonte
-1

O problema fundamental é que você está tentando usar um modelo de tipos incorreto. Apesar da redação do padrão ISO C ++ e das idéias errôneas expressas originalmente por Bjarne no ARM, não existe um tipo const. Ao contrário, constness é uma propriedade de ponteiros apenas (e referências, mas deixa-os por simplicidade). Portanto, uma classe que representa um tipo const e não const é um conceito errado.

Const no próprio C ++ é um conceito inseguro, ele deveria ter sido removido. Isso fica evidente na Biblioteca Padrão ao tentar modelar o conceito genericamente. Ele só funciona um nível profundo como você observou. Aplica-se ao caminho de acesso a um objeto, mas não se propaga para uma estrutura de dados (coleção vinculada de objetos). O exemplo mais simples é uma lista unicamente ligada: um ponteiro const para um nó de lista gera imediatamente um próximo ponteiro não const.

Em um modelo teórico, as abstrações const e non-const distinguem-se pela ausência ou presença de métodos mutadores e, portanto, são tipos distintos sem qualquer relação "isA", o que quer que seja.

Meu melhor conselho: pare de tentar modelar abstrações com técnicas OO, porque o OO é comprovadamente tão restrito na aplicabilidade que não é adequado para modelar qualquer um, exceto os conceitos mais simples. Aprenda alguns métodos de programação funcional.

    
por 05.10.2015 / 18:20
fonte