English
std::variant & std::visit

std::variant & std::visit

Summary of Variant and Visit, Added in C++17

Additional Information

[Related Documentation](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0088r3.html

)

The link above contains information about Variant, which was added starting with C++17, and the reasons behind its addition. To put it simply, the discussion revolves around the fact that Boost had been using this functionality for far too long—something that should have been included as std::optional—and the consensus was to stop dragging it out and just add it to the standard as soon as possible.

Internal Structure

1. Variant

  • Data Buffer A memory space large enough to store the largest type among those specified as template arguments; it is generally implemented as a union.

  • Identifier An index or identifier that indicates which type of object is currently stored in the data buffer. By using this identifier, std::variant can ensure type safety.

By having a sufficiently large memory buffer, the variant type can perform the following actions.


	struct Tracer
	{
		std::string name;

		Tracer(const std::string& n) : name(n)
		{
			std::cout << "  [+] '" << name << "' Tracer created (Constructor)\n";
		}

		// Copy Constructor
		Tracer(const Tracer& other) : name(other.name)
		{
			std::cout << "  [*] '" << name << "' Tracer copy-constructed (Copy Constructor)\n";
		}

		~Tracer()
		{
			std::cout << "  [-] '" << name << "' Tracer destroyed (Destructor)\n";
		}
	};

	// 1. Initialize the variant with a Tracer type.
	std::cout << "1. Initializing variant with Tracer(\"Apple\").\n";
	std::variant<int, Tracer> var = Tracer("Apple");
	std::cout << "   Variant now holds Tracer(\"Apple\").\n\n";

	// 2. Assign a value of a different type (int).
	std::cout << "2. Assigning integer 100 to the variant.\n";
	var = 100;
	std::cout << "   Variant now holds integer 100.\n\n";

	// 3. Assign another Tracer type value again.
	std::cout << "3. Assigning Tracer(\"Banana\") to the variant.\n";
	var = Tracer("Banana");
	std::cout << "   Variant now holds Tracer(\"Banana\").\n\n";

	std::cout << "4. main function is about to end.\n";
	return 0;

When you run code like this, you get the following result.

1. Initializing variant with Tracer("Apple").
  [+] 'Apple' Tracer created (Constructor)
  [*] 'Apple' Tracer copy-constructed (Copy Constructor)
  [-] 'Apple' Tracer destroyed (Destructor)
   Variant now holds Tracer("Apple").

2. Assigning integer 100 to the variant.
  [-] 'Apple' Tracer destroyed (Destructor)
   Variant now holds integer 100.

3. Assigning Tracer("Banana") to the variant.
  [+] 'Banana' Tracer created (Constructor)
  [*] 'Banana' Tracer copy-constructed (Copy Constructor)
  [-] 'Banana' Tracer destroyed (Destructor)
   Variant now holds Tracer("Banana").

4. main function is about to end.
  [-] 'Banana' Tracer destroyed (Destructor)

You can observe the sequence where the destructor for the existing value is called first, followed by the creation of a temporary object on the stack, and finally, the copied value is moved into its own memory buffer.

Therefore, by internally reserving space for the largest class using a union, the variant type can be used without issues regardless of what is passed in!

2. Visit

Although this varies by library,

  • Function pointer table

  • Switch statement

It is generally categorized into these two main forms. In reality, aside from slight differences in implementation, overhead, and compiler optimization, it can be understood that they are fundamentally implemented to call the appropriate function based on the active type determined by the discriminator.

댓글 작성

게시글에 대한 의견을 남겨 주세요.

댓글 0