Meu compilador ignorou meu membro da classe thread_local estático não utilizado?

10

Quero fazer algum registro de thread na minha turma, por isso decido adicionar uma verificação para o thread_localrecurso:

#include <iostream>
#include <thread>

class Foo {
 public:
  Foo() {
    std::cout << "Foo()" << std::endl;
  }
  ~Foo() {
    std::cout << "~Foo()" << std::endl;
  }
};

class Bar {
 public:
  Bar() {
    std::cout << "Bar()" << std::endl;
    //foo;
  }
  ~Bar() {
    std::cout << "~Bar()" << std::endl;
  }
 private:
  static thread_local Foo foo;
};

thread_local Foo Bar::foo;

void worker() {
  {
    std::cout << "enter block" << std::endl;
    Bar bar1;
    Bar bar2;
    std::cout << "exit block" << std::endl;
  }
}

int main() {
  std::thread t1(worker);
  std::thread t2(worker);
  t1.join();
  t2.join();
  std::cout << "thread died" << std::endl;
}

O código é simples. Minha Barclasse tem um thread_localmembro estático foo. Se uma estática thread_local Foo foofor criada, isso significa que uma thread é criada.

Mas quando executo o código, nada aparece nas Foo()impressões e se eu remover o comentário no Barconstrutor de que usa foo, o código funciona bem.

Eu tentei isso no GCC (7.4.0) e Clang (6.0.0) e os resultados são os mesmos. Eu acho que o compilador descobriu que foonão é usado e não gera uma instância. assim

  1. O compilador ignorou o static thread_localmembro? Como posso depurar para isso?
  2. Em caso afirmativo, por que um staticmembro normal não tem esse problema?
ravenisadesk
fonte

Respostas:

9

Não há nenhum problema com sua observação. [basic.stc.static] / 2 proíbe a eliminação de variáveis ​​com duração de armazenamento estático:

Se uma variável com duração de armazenamento estático tiver inicialização ou um destruidor com efeitos colaterais, ela não deverá ser eliminada, mesmo que pareça não ser usada, exceto que um objeto de classe ou sua cópia / movimentação pode ser eliminada conforme especificado em [class.copy] .

Essa restrição não está presente para outras durações de armazenamento. De fato, [basic.stc.thread] / 2 diz:

Uma variável com duração de armazenamento do encadeamento deve ser inicializada antes do seu primeiro uso de odr e, se construída , deve ser destruída na saída do encadeamento.

Isso sugere que uma variável com duração de armazenamento do encadeamento não precisa ser construída, a menos que o odr seja usado.


Mas por que essa discrepância?

Para a duração do armazenamento estático, há apenas uma instância de uma variável por programa. Os efeitos colaterais de sua construção podem ser significativos (como um construtor em todo o programa), portanto, os efeitos colaterais são necessários.

Para a duração do armazenamento local do encadeamento, no entanto, há um problema: um algoritmo pode iniciar muitos encadeamentos. Para a maioria desses threads, a variável é completamente irrelevante. Seria hilário se uma biblioteca de simulação de física externa que telefonar std::reduce(std::execution::par_unseq, first, last)acabasse criando muitas fooinstâncias, certo?

Obviamente, pode haver um uso legítimo para efeitos colaterais da construção de variáveis ​​de duração do armazenamento local do encadeamento que não são usadas por odr (por exemplo, um rastreador de encadeamentos). No entanto, a vantagem de garantir isso não é suficiente para compensar a desvantagem acima mencionada; portanto, essas variáveis ​​podem ser eliminadas desde que não sejam usadas por odr. (Porém, seu compilador pode optar por não fazer. E você também pode criar seu próprio invólucro std::threadque cuide disso.)

LF
fonte
11
Ok ... se o padrão disse isso, que assim seja ... Mas não é estranho o comitê não considerar os efeitos colaterais como duração estática do armazenamento?
Ravenisadesk #
@reavenisadesk Ver resposta atualizada.
LF
1

Encontrei essas informações em " ELF Handling for Thread-Local Storage ", que pode provar a resposta da @LF

Além disso, o suporte em tempo de execução deve evitar a criação do armazenamento local do encadeamento, se não for necessário. Por exemplo, um módulo carregado pode ser usado apenas por um encadeamento dos muitos que compõem o processo. Seria um desperdício de memória e tempo alocar o armazenamento para todos os threads. Um método preguiçoso é desejado. Isso não representa muita carga extra, pois o requisito de manipular objetos carregados dinamicamente já requer reconhecimento de armazenamento que ainda não foi alocado. Essa é a única alternativa para parar todos os threads e alocar armazenamento para todos os threads antes de deixá-los executar novamente.

ravenisadesk
fonte