Utilize std::any do C++ Moderno nos seus projetos

🕳️ Dê adeus de uma vez por todas ao `void*`.


Utilize std::any do C++ Moderno nos seus projetos


std::any é um recurso da biblioteca padrão C++ que foi introduzido no C++17.

Este componente pertence ao conjunto de classes de contêineres com segurança de tipo, fornecendo um meio seguro para armazenar e manipular valores de qualquer tipo.

Ele é especialmente útil quando você precisa lidar com situações em que o tipo da variável pode(pleonasmo): variar! 😃

Aí você diz:

— Ah, cara! De boa. Para esses casos eu uso o void *.

Sim, realmente você tem razão, mas você já viu como a nova geração está em relação a segurança de memória ???

Lembrando que o termo segurança é usado em Português, pois não existe uma palavra que se adeque a tradução para Safe, ou seja: Safe != Seguro)! 😛

Sem dizer que void * é realmente perigoso!

Se você fizer isso, funciona:

void * some_data; // Péssima ideia

std::string str = "Oi";
int x = 3;
decltype(x) y = 6;

some_data = &str;
std::cout << *(std::string*)some_data << '\n';

some_data = &x;
std::cout << *(int*)some_data << '\n';

some_data = &y;
std::cout << "Tipo de y: " << typeid(y).name() << '\n'; // include typeinfo

Mas, a chance de isso dar mer%$a é grande! Ao final do uso dessas variáveis, some_data vai continuar existindo, ou seja, tempo de vida indefinido!

E é para subsitituir o void* que o std::any foi criado no C++ Moderno que, com certeza, é totalmente Safe!

Em outras palavras, ele é um wrapper que encapsula sua variável para um shared_ptr(ponteiros inteligentes) da vida! Sim, e existe até um std::make_any!!!


Como utilizar o std::any

Primeiramente você precisa incluir o cabeçalho dele:

Logicamente, só funciona a partir do C++17 como foi dito no início!

#include <any>

E agora o mesmo código que foi apresentado acima, mas usando std::any:

#include <iostream>
#include <any>

int main(){
  std::any some_data;

  std::string str = "Oi";
  int x = 3;
  auto y = std::make_any<decltype(x)>(6);

  some_data = str;
  std::cout << std::any_cast<std::string>(some_data) << '\n';

  some_data = x;
  std::cout << std::any_cast<int>(some_data) << '\n';

  some_data = y;
  std::cout << "Tipo de y: " << some_data.type().name() << '\n';
}

No código acima vimos que:

  • std::any some_data; - Declara a variável;
  • std::any_cast<T>(some_data) - Converte para o tipo desejado;
  • std::make_any<T> - Outra forma de criar objetos;
  • some_data.type().name() - Obtém o tipo de dado sem precisar de typeinfo.

E você pode usar pra absolutamente tudo: std::vector, Lambda e tudo que existir de tipo de dado!

E o cara pergunta outra coisa:

— Tá! E se eu quiser acabar o tempo de vida do std::any manualmente?

Basta usar a estrutura de união reset ou até mesmo com o operador de incialização:

some_data.reset();
// Ou
some_data = {};

— E pra verificar se std::any está vazio? Use has_value():

std::cout << (some_data.has_value() ? "Cheio!" : "Vazio.") << '\n';

O type() sem união com name() pode ser usado para comparar tipos:

std::cout << (some_data.type() == typeid(void)) << '\n'; // 0 pra false
std::cout << (some_data.type() == typeid(int)) << '\n'; // 1 pra true

Para usar os nomes booleanos use: std::cout << std::boolalpha << (some_data.type() == typeid(int)) << '\n';.

Para lançar exceções você deve usar o std::bad_any_cast:

try {

  std::any any_str("Oiii");
  auto my_any{ std::make_any<std::string>(any_str.type().name()) };
  std::cout << std::any_cast<std::string>(my_any) << '\n';

}catch (const std::bad_any_cast& e) {
  std::cerr << "Error: " << e.what() << std::endl;
}

Para verificar se realmente tudo está entre os conformes, nunca se esqueça de usar as flags para seu compilador: -Wall -Wextra -pedantic -g -fsanitize=address.


Exemplo na vida real

Imagine você ter um código que precisa concatenar vários tipos e retornar uma string. No entanto, um dos tipos pode ser: int, double ou std::string.

Se usar std::any_cast<T> no retorno assim:

Exemplo:

#include <iostream>
#include <any>
#include <sstream>

enum class Message {
  SUCCESS,
  WARNING,
  ERROR,
  UNKNOW
};

std::string add_info(Message, const std::string&, std::any, int);

int main(){
  std::any obj;

  obj = std::string("Start");
  std::cout << add_info(Message::SUCCESS, " do tipo string: ", obj, 3) << '\n';

  obj = 6;
  std::cout << add_info(Message::WARNING, " do tipo int: ", obj, 9) << '\n';

  obj = 3.14;
  std::cout << add_info(Message::ERROR, " do tipo double: ", obj, 0) << '\n';

  obj.reset();
  std::cout << add_info(Message::UNKNOW, " sem tipo: ", obj, 9) << '\n';

  obj = "CONST_CHAR";
  std::cout << add_info(Message::SUCCESS, " sem tipo: ", nullptr, 9) << '\n';

  return 0;
}

std::string add_info(Message msg, const std::string& out, std::any object, int num){
  return std::any_cast<std::string>(msg) + out + "'" + std::any_cast<std::string>(object) + "' " + std::to_string(num);
}

Compile: g++ -Wall -Wextra -pedantic -g -fsanitize=address main.cpp.

Terá um std::bad_any_cast na saída:

terminate called after throwing an instance of 'std::bad_any_cast'
  what():  bad any_cast
Abortado

Ambas conversões(da msg e da object) estão incorretas:

std::any_cast<std::string>(msg) + ...
// Quanto
std::any_cast<std::string>(object)

Você precisa fazer um switch case para o enumerador e no caso do parâmetro object: Precisará usar o has_value(), armazenar o type() em std::type_info& e usar um std::stringstream para atribuir o tipos de retorno com união ao: str(), assim:

#include <iostream>
#include <any>
#include <sstream>

enum class Message {
  SUCCESS,
  WARNING,
  ERROR,
  UNKNOW
};

std::string add_info(Message, const std::string&, std::any, int);

int main(){
  std::any obj;

  obj = std::string("Start");
  std::cout << add_info(Message::SUCCESS, " do tipo string: ", obj, 3) << '\n';

  obj = 6;
  std::cout << add_info(Message::WARNING, " do tipo int: ", obj, 9) << '\n';

  obj = 3.14;
  std::cout << add_info(Message::ERROR, " do tipo double: ", obj, 0) << '\n';

  obj.reset();
  std::cout << add_info(Message::UNKNOW, " sem tipo: ", obj, 9) << '\n';

  obj = "CONST_CHAR";
  std::cout << add_info(Message::SUCCESS, " sem tipo: ", nullptr, 9) << '\n';

  return 0;
}

std::string add_info(Message msg, const std::string& out, std::any object, int num){
  std::string local_msg {"NOTHING"};
  std::stringstream ss;

  switch (msg){
    case Message::SUCCESS:
      local_msg = "SUCCESS";
      break;
    case Message::WARNING:
      local_msg = "WARNING";
      break;
    case Message::ERROR:
      local_msg = "ERROR";
      break;
    case Message::UNKNOW:
      local_msg = "UNKNOW";
      break;
  }

  
  if (object.has_value()) {
    const std::type_info& type = object.type();
    if (type == typeid(std::string)) {
      ss << std::any_cast<std::string>(object);
    } else if (type == typeid(int)) {
      ss << std::any_cast<int>(object);
    } else if (type == typeid(double)) {
      ss << std::any_cast<double>(object);
    } else {
      ss << "null";
    }
  } else {
    ss << "[no object]";
  }

  return local_msg + out + "'" + ss.str() + "' " + std::to_string(num);
}

Que função!!! :O , mas assim seu código ficará safe! Compile: g++ -Wall -Wextra -pedantic -g -fsanitize=address main.cpp e após rodar ./a.out, a saída será o esperado:

SUCCESS do tipo string: 'Start' 3
WARNING do tipo int: '6' 9
ERROR do tipo double: '3.14' 0
UNKNOW sem tipo: '[no object]' 9
SUCCESS sem tipo: 'null' 9

Parece trabalhoso, mas essa é a forma correta de você finalizar o tempo de vida de um tipo qualquer!


Além de totalmente SAFE, o std::any é muito prático e uma mão na roda!

Em alguns casos também é interessante usar std::variant e obter qual o tipo com .index(), ex.:

//...
std::variant <int, std::string, double> var;
//...
if(var.index() == 1){
  std::cout << "var é uma std::string\n";
  break;
//...

Escolha std::any para flexibilidade máxima e quando os tipos armazenados são desconhecidos até o tempo de execução. Escolha std::variant para maior segurança e desempenho quando você sabe todos os tipos possíveis em tempo de compilação.

Para mais informações acesse: https://en.cppreference.com/w/cpp/utility/any


cpp cppdaily


Compartilhe


Nosso canal no Youtube

Inscreva-se


Marcos Oliveira

Marcos Oliveira

Desenvolvedor de software
https://github.com/terroo


Crie Aplicativos Gráficos para Linux e Windows com C++

Aprenda C++ Moderno e crie Games, Programas CLI, GUI e TUI de forma fácil.

Saiba Mais

Receba as novidades no seu e-mail!

Após cadastro e confirmação do e-mail, enviaremos semanalmente resumos e também sempre que houver novidades por aqui para que você mantenha-se atualizado!