Sequência constante estática (membro da classe)

444

Eu gostaria de ter uma constante estática privada para uma classe (neste caso, uma fábrica de formas).

Eu gostaria de ter algo do tipo.

class A {
   private:
      static const string RECTANGLE = "rectangle";
}

Infelizmente, recebo todos os tipos de erro do compilador C ++ (g ++), como:

O ISO C ++ proíbe a inicialização do membro 'RECTANGLE'

inicialização inválida na classe do membro de dados estáticos do tipo não integral 'std :: string'

erro: tornando 'RECTANGLE' estático

Isso me diz que esse tipo de design de membro não é compatível com o padrão. Como você tem uma constante literal privada (ou talvez pública) sem precisar usar uma diretiva #define (eu quero evitar a feia da globalidade dos dados!)

Qualquer ajuda é apreciada.

Libra.
fonte
15
Obrigado por todas as suas ótimas respostas! Viva assim!
lb.
Alguém pode me dizer o que é um tipo 'integral'? Muito obrigado.
lb.
1
Tipos integrais refere-se a tipos que representam números inteiros. Consulte publib.boulder.ibm.com/infocenter/comphelp/v8v101/…
bleater
Seqüência de caracteres estática privada em sua fábrica não é uma boa solução - considere que seus clientes de fábrica terão que saber quais formas são suportadas; portanto, em vez de mantê-las em estática privada, coloque-as em um espaço de nome separado como const std :: string RECTANGLE = "Rectangle "
LukeCodeBaker
se sua classe é uma classe de modelo, em seguida, ver stackoverflow.com/q/3229883/52074
Trevor Boyd Smith

Respostas:

469

Você precisa definir seu membro estático fora da definição de classe e fornecer o inicializador lá.

Primeiro

// In a header file (if it is in a header file in your case)
class A {   
private:      
  static const string RECTANGLE;
};

e depois

// In one of the implementation files
const string A::RECTANGLE = "rectangle";

A sintaxe que você estava originalmente tentando usar (inicializador dentro da definição de classe) é permitida apenas nos tipos integral e enum.


A partir do C ++ 17, você tem outra opção, que é bastante semelhante à sua declaração original: variáveis ​​inline

// In a header file (if it is in a header file in your case)
class A {   
private:      
  inline static const string RECTANGLE = "rectangle";
};

Nenhuma definição adicional é necessária.

Ou, em vez de constvocê, pode declarar constexprnesta variante. Explícito inlinenão seria mais necessário, pois constexprimplica inline.

Formiga
fonte
8
Além disso, se não houver requisito para usar uma string STL, você também pode definir apenas um const char *. (menos sobrecarga)
KSchmidt
50
Não tenho certeza se é sempre menos sobrecarga - depende do uso. Se esse membro tiver que ser passado como argumento para funções que usam const string &, haverá temporariamente criado para cada chamada versus criação de um objeto string durante a inicialização. A sobrecarga do IMHO para a criação de um objeto de sequência estática é negligenciável.
Tadeusz Kopec
23
Eu prefiro usar std :: string o tempo todo também. A sobrecarga é insignificante, mas você tem muito mais opções e são muito menos propensos a escrever algumas coisas tolo como "mágica" == A :: RECTANGLE só para comparar o seu endereço ...
Matthieu M.
9
o char const*tem a bondade de ser inicializado antes que toda a inicialização dinâmica seja concluída. Portanto, no construtor de qualquer objeto, você pode confiar RECTANGLEpara ter sido inicializado já.
Johannes Schaub - litb
8
@cirosantilli: Porque, desde o início dos tempos, os inicializadores de C ++ faziam parte das definições , não das declarações . E a declaração de membro de dados dentro da classe é apenas isso: uma declaração. (Por outro lado, uma exceção foi feita por membros integrais e enum const, e em C ++ 11 - para os membros de const literais tipos.)
AnT
153

No C ++ 11, você pode fazer agora:

class A {
 private:
  static constexpr const char* STRING = "some useful string constant";
};
abyss.7
fonte
30
Infelizmente, esta solução não funciona para std :: string.
HelloWorld
2
Observe que 1. isso funciona apenas com literais e 2. isso não está em conformidade com o padrão, embora o Gnu / GCC complique multas, outros compiladores geram um erro. A definição tem que estar no corpo.
ManuelSchneid3r
2
@ ManuelSchneid3r Como exatamente isso "não está em conformidade"? Parece- me uma inicialização brace-or-equal padrão C ++ 11 padrão para mim.
underscore_d
3
@rvighne, não, isso está incorreto. constexprimplica constpara var, não para o tipo que aponta. Ou seja, static constexpr const char* consté o mesmo que static constexpr const char*, mas não é o mesmo que static constexpr char*.
midenok
2
@ abyss.7 - Obrigado por sua resposta, e eu tenho outra, por favor: Por que tem que ser estático?
Guy Avraham
34

Definições de classe interna, você só pode declarar membros estáticos. Eles precisam ser definidos fora da classe. Para constantes integrais em tempo de compilação, o padrão faz a exceção de que você pode "inicializar" membros. Ainda não é uma definição, no entanto. Tomar o endereço não funcionaria sem definição, por exemplo.

Eu gostaria de mencionar que não vejo o benefício de usar std :: string sobre const char [] para constantes . std :: string é bom e tudo, mas requer inicialização dinâmica. Então, se você escrever algo como

const std::string foo = "hello";

no escopo do espaço para nome, o construtor foo será executado logo antes da execução das principais partidas e esse construtor criará uma cópia da constante "hello" na memória heap. A menos que você realmente precise de RECTANGLE para ser um std :: string, você também pode escrever

// class definition with incomplete static member could be in a header file
class A {
    static const char RECTANGLE[];
};

// this needs to be placed in a single translation unit only
const char A::RECTANGLE[] = "rectangle";

Lá! Sem alocação de heap, sem cópia, sem inicialização dinâmica.

Saúde, s.

sellibitze
fonte
1
Esta é a resposta pré C ++ 11. Use C ++ padrão e use std :: string_view.
1
O C ++ 11 não possui std :: string_view.
Lukas Salich 08/04/19
17

Isso é apenas uma informação extra, mas se você realmente deseja a string em um arquivo de cabeçalho, tente algo como:

class foo
{
public:
    static const std::string& RECTANGLE(void)
    {
        static const std::string str = "rectangle";

        return str;
    }
};

Embora eu duvide que seja recomendado.

GManNickG
fonte
Isso parece legal :) - estou supondo que você tenha experiência em outras línguas além de c ++?
lb.
5
Eu não recomendaria. Eu faço isso frequentemente. Funciona bem e acho mais óbvio do que colocar a string no arquivo de implementação. Os dados reais de std :: string ainda estão localizados no heap. Eu retornaria um const char *, caso em que você não precisa declarar a variável estática para que a declaração ocupe menos espaço (código). Apenas uma questão de gosto.
Zoomulator
15

No C ++ 17, você pode usar variáveis ​​embutidas :

class A {
 private:
  static inline const std::string my_string = "some useful string constant";
};

Observe que isso é diferente da resposta do abyss.7 : Este define um std::stringobjeto real , não umconst char*

Oz Solomon
fonte
Você não acha que o uso inlinecriará muitas duplicatas?
Shuva 19/08/19
1
@shuva Não, a variável não será duplicada .
Zett42 10/10
8

Para usar essa sintaxe de inicialização em classe, a constante deve ser uma const estática do tipo integral ou de enumeração inicializada por uma expressão constante.

Essa é a restrição. Portanto, nesse caso, você precisa definir variáveis ​​fora da classe. consulte resposta de @AndreyT

aJ.
fonte
7

As variáveis ​​estáticas da classe podem ser declaradas no cabeçalho, mas devem ser definidas em um arquivo .cpp. Isso ocorre porque pode haver apenas uma instância de uma variável estática e o compilador não pode decidir em qual arquivo de objeto gerado o colocará, portanto, você deve tomar a decisão.

Para manter a definição de um valor estático com a declaração em C ++ 11, uma estrutura estática aninhada pode ser usada. Nesse caso, o membro estático é uma estrutura e deve ser definido em um arquivo .cpp, mas os valores estão no cabeçalho.

class A
{
private:
  static struct _Shapes {
     const std::string RECTANGLE {"rectangle"};
     const std::string CIRCLE {"circle"};
  } shape;
};

Em vez de inicializar membros individuais, toda a estrutura estática é inicializada em .cpp:

A::_Shapes A::shape;

Os valores são acessados ​​com

A::shape.RECTANGLE;

ou - como os membros são privados e devem ser usados ​​apenas em A - com

shape.RECTANGLE;

Observe que essa solução ainda sofre com o problema da ordem de inicialização das variáveis ​​estáticas. Quando um valor estático é usado para inicializar outra variável estática, a primeira ainda não pode ser inicializada.

// file.h
class File {
public:
  static struct _Extensions {
    const std::string h{ ".h" };
    const std::string hpp{ ".hpp" };
    const std::string c{ ".c" };
    const std::string cpp{ ".cpp" };
  } extension;
};

// file.cpp
File::_Extensions File::extension;

// module.cpp
static std::set<std::string> headers{ File::extension.h, File::extension.hpp };

Nesse caso, os cabeçalhos das variáveis ​​estáticas conterão {""} ou {".h", ".hpp"}, dependendo da ordem de inicialização criada pelo vinculador.

Conforme mencionado por @ abyss.7, você também pode usar constexprse o valor da variável puder ser calculado em tempo de compilação. Mas se você declarar suas strings com static constexpr const char*e seu programa usar de std::stringoutra forma, haverá uma sobrecarga porque um novo std::stringobjeto será criado toda vez que você usar essa constante:

class A {
public:
   static constexpr const char* STRING = "some value";
};
void foo(const std::string& bar);
int main() {
   foo(A::STRING); // a new std::string is constructed and destroyed.
}
Marko Mahnič
fonte
Resposta bem preparada Marko. Dois detalhes: um não precisa de arquivos cpp para membros da classe estática e também use std :: string_view para qualquer tipo de constante.
4

O padrão atual permite apenas essa inicialização para tipos integrais constantes estáticos. Então você precisa fazer o que o AndreyT explicou. No entanto, isso estará disponível no próximo padrão através da sintaxe de inicialização do novo membro .

Leandro TC Melo
fonte
4

possível apenas faça:

static const std::string RECTANGLE() const {
    return "rectangle";
} 

ou

#define RECTANGLE "rectangle"
chikuba
fonte
11
Usar #define quando uma constante digitada pode ser usada está errado.
Artur Czajka
Seu primeiro exemplo é basicamente uma boa solução, se você não tiver, constexprmas não puder criar uma função estática const.
Frank soprador
Esta solução deve ser evitada. Ele cria uma nova string em cada chamada. Isso seria melhor:static const std::string RECTANGLE() const { static const std::string value("rectangle"); return value; }
Oz Solomon
Por que usar o contêiner completo como valor de retorno? Use std :: string_vew .. seu conteúdo permanecerá válido nesse caso. ainda melhor usar literais de string para criar e retornar a exibição de string ... e por último mas não menos importante, o valor de retorno const não tem significado ou efeito aqui .. ah sim, e tem isso como inline, não estático, em algum cabeçalho chamado namespace ... e por favor, fazê-lo para ser constexpr
4

Você pode optar pela const char*solução mencionada acima, mas se precisar de strings o tempo todo, terá muita sobrecarga.
Por outro lado, a cadeia estática precisa de inicialização dinâmica; portanto, se você quiser usar seu valor durante a inicialização de outra variável global / estática, poderá encontrar o problema da ordem de inicialização. Para evitar isso, o mais barato é acessar o objeto de cadeia estática por meio de um getter, que verifica se o seu objeto foi inicializado ou não.

//in a header  
class A{  
  static string s;   
public:   
  static string getS();  
};  
//in implementation  
string A::s;  
namespace{  
  bool init_A_s(){  
    A::s = string("foo");   
    return true;  
  }  
  bool A_s_initialized = init_A_s();  
}  
string A::getS(){      
  if (!A_s_initialized)  
    A_s_initialized = init_A_s();  
  return s;  
}  

Lembre-se de usar apenas A::getS(). Como qualquer encadeamento só pode ser iniciado main()e A_s_initializedinicializado antes main(), você não precisa de bloqueios, mesmo em um ambiente multithread. A_s_initializedé 0 por padrão (antes da inicialização dinâmica); portanto, se você usar getS()antes da inicialização de s, chame a função init com segurança.

Aliás, na resposta acima: " estática const std :: string RECTANGLE () const ", funções estáticas não podem ser constporque não podem alterar o estado de qualquer objeto de qualquer maneira (não existe esse ponteiro).

user2806882
fonte
4

Avanço rápido para 2018 e C ++ 17.

  • não use std :: string, use literais std :: string_view
  • observe o abaixo 'constexpr'. Este também é um mecanismo de "tempo de compilação".
  • sem inline não significa repetição
  • nenhum arquivo cpp não é necessário para isso
  • static_assert 'funciona' somente em tempo de compilação

    using namespace std::literals;
    
    namespace STANDARD {
    constexpr 
    inline 
    auto 
    compiletime_static_string_view_constant() {
    // make and return string view literal
    // will stay the same for the whole application lifetime
    // will exhibit standard and expected interface
    // will be usable at both
    // runtime and compile time
    // by value semantics implemented for you
        auto when_needed_ =  "compile time"sv;
        return when_needed_  ;
    }

    };

Acima está um cidadão C ++ padrão adequado e legal. Ele pode se envolver prontamente em todo e qualquer algoritmo std ::, contêineres, utilitários e similares. Por exemplo:

// test the resilience
auto return_by_val = []() {
    auto return_by_val = []() {
        auto return_by_val = []() {
            auto return_by_val = []() {
return STANDARD::compiletime_static_string_view_constant();
            };
            return return_by_val();
        };
        return return_by_val();
    };
    return return_by_val();
};

// actually a run time 
_ASSERTE(return_by_val() == "compile time");

// compile time 
static_assert(
   STANDARD::compiletime_static_string_view_constant() 
   == "compile time" 
 );

Aproveite o C ++ padrão


fonte
Use std::string_viewpara constantes apenas se você usar string_viewparâmetros em todas as suas funções. Se alguma de suas funções usar um const std::string&parâmetro, uma cópia de uma sequência será criada quando você passar uma string_viewconstante por esse parâmetro. Se suas constantes forem do tipo, std::stringas cópias não serão criadas nem para const std::string&parâmetros nem para std::string_viewparâmetros.
Marko Mahnič 23/08/19
Boa resposta, mas curioso para saber por que o string_view está sendo retornado de uma função? Esse tipo de truque foi útil antes que as inlinevariáveis ​​chegassem ao C ++ 17 com sua semântica de ODR. Mas string_view é C ++ 17 também, por isso só constexpr auto some_str = "compile time"sv;faz o trabalho (e, na verdade, não é uma variável, é constexpr, por isso, inlineestá implícito, se você tem uma variável - ou seja, não constexpr- então inline auto some_str = "compile time"sv;vai fazê-lo, embora, naturalmente, um namespace escopo variável, que é essencialmente uma variável global, raramente seria uma boa ideia).
Mentalidade de perda 11/04