3

I have a class T, it looks like this:

class T {
  uint64_t id;
  std::string description;
  char status;
  uint64_t createdAt;
  uint64_t updatedAt;

T(uint64_t c_id, std::string_view c_description, char c_status, uint64_t c_createdAt, uint64_t c_updatedAt) :
    id(c_id),
    description(c_description),
    status(c_status),
    createdAt(c_createdAt),
    updatedAt(c_updatedAt) {}

T(T&& other) :
    id(other.id),
    description(std::move(other.description)),
    status(other.status),
    createdAt(other.createdAt),
    updatedAt(other.updatedAt) {}
  // member functions
};

At some point, I need to append an optional into a vector. What is the best way between these options (or are there other options)?

std::optional<T> opt = myFunction();
std::vector<T> tasks;
if(opt.has_value()) {
  tasks.push_back(*opt);
  tasks.push_back(opt.value());
  tasks.push_back(std::move(*opt));

  tasks.emplace_back(*opt);
  tasks.emplace_back(opt.value());
  tasks.emplace_back(std::move(*opt));
}
4
  • 4
    "best" by which metric? Also, what should happen if the optional is empty? Commented Mar 25 at 9:16
  • 1
    Are you sure the optional is non-empty? Also important but omitted: does T have a move constructor? You left out all member functions, including the most important one for this question. Commented Mar 25 at 9:17
  • The snippet is just a recreation of my program, so I didn't add the check in there. I've also added the relevant constructors of T. Commented Mar 25 at 9:50
  • Offtopic: Be aware that T symbol name is usually used as template parameter. So when you use this as actual class name this is vary confusing for may developers. You should use Foo or Bar. Commented Mar 25 at 11:38

3 Answers 3

9

In C++26, the simplest way to copy-insert the contents of opt is

std::optional<T> opt = myFunction();
std::vector<T> tasks;
tasks.append_range(opt);

And the simplest way to move-insert the contents of opt is

std::optional<T> opt = myFunction();
std::vector<T> tasks;
tasks.append_range(opt | std::views::as_rvalue);
Sign up to request clarification or add additional context in comments.

9 Comments

How is std::ranges::move(opt, std::back_inserter(tasks)); simpler than tasks.push_back(std::move(*opt));?
@DominikKaszewski it doesn't assume opt has a value, under pain of undefined behaviour
@DominikKaszewski It wasn't when I wrote this answer. The point is that checking isn't desirable.
So, std::optional<T> is viewed as a range of 0 or 1 element.
@Jarod42 yes, which is great for things like this, or range | transform(func_returining_optional) | join
|
8
// copies optional content into vector, optional unchanged
// undefined behavior if optional was originally empty
tasks.push_back(*opt); 

// copies optional content into vector, optional unchanged
// throws std::bad_optional_access if optional is empty
tasks.push_back(opt.value());

// moves the optional content into vector, opt std::string member is now empty
// undefined behavior if optional was originally empty
tasks.push_back(std::move(*opt));

// moves the optional content into vector, opt std::string member is now empty
// throws std::bad_optional_access if optional is empty
tasks.push_back(std::move(opt.value()));

the cheapest (generating the least amount of assembly instructions) would be

if (opt) // get rid of UB in operator*
{
  tasks.push_back(std::move(*opt)); // move the contents of opt into tasks
}
// bonus: same syntax as a pointer!

there is no difference between push_back and emplace_back in this context. emplace_back is only useful when the type of the argument is different than the type of the container and you want to have a conversion. push_back will do a conversion then a move, whereas emplace_back will just do a conversion. Don't abuse emplace_back as it does explicit conversions implicitly.

Comments

-1

Depending on your need, it might also be simpler to just hold a list of optionals

std::optional<T> opt = myFunction();
std::vector<std::optional<T>> maybeTasks;
maybeTasks.push_back(opt);

and worry later about wether they exist, or if all tasks are created and then handled separately:

std::vector<T> taskList;
for (auto it : maybeTasks) {
    if(it.has_value()) {
        taskList.push_back(it.value());
    }
}

3 Comments

This just costs memory, reduces cache locality and performance. The check is also very simple and should be done before inserting. It's only a matter of how to insert.
It is definitely not code made for performance. It is however pretty readable and beginner-friendly, and depending on the application, sometimes good enough in my opinion.
But still you are doing the exact same thing just in the wrong order. It costs you nothing to do the if has_value before inserting. It is objectively better with no downsides.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.