tl::expected
Library
tl::expected
Overview
tl::expected is a backport implementation of C++23's std::expected, providing functional programming-style error handling. It is a type that can represent both successful values and errors, enabling safe error handling without exceptions. Developed by Sy Brand (TartanLlama), it can be used with C++11/14/17. It supports monadic operations (map, and_then, etc.) and allows error handling logic to be separated as side effects, making it compatible with functional programming paradigms and supporting the creation of robust code.
Details
tl::expected is a template class that holds either a success value (T) or an error value (E). Similar to std::optional, but differs in that it can provide detailed error information when a value doesn't exist. Through monadic operations, error handling can be separated from function chains, allowing you to write code that focuses on the success path. The exception-free error handling pattern allows safe error processing in performance-critical scenarios or environments where exceptions are prohibited. When combined with validation functions, powerful validation functionality can be built.
Key Features
- Exception-free Error Handling: Safe error processing without exceptions
- Functional Programming: Function chaining through monadic operations
- Type Safety: Compile-time enforcement of error handling
- Performance: No exception handling overhead
- C++11 Support: Usable with older compilers
- Header-only: Easy to integrate
Pros and Cons
Pros
- Predictable performance as it doesn't use exceptions
- Error handling is enforced at compile time
- Clean code with functional programming style
- Type-safe handling of detailed error information
- Easy integration as header-only
- Early implementation of C++23 standard
Cons
- Learning curve for functional programming concepts
- Integration effort with existing exception-based code
- Difficult to obtain stack traces during debugging
- Unfamiliar concept for some developers
- Potentially verbose description for complex error handling
References
Code Examples
Installation and Basic Setup
# Install using vcpkg
vcpkg install tl-expected
# Manually download header file
wget https://github.com/TartanLlama/expected/releases/download/v1.0.0/tl-expected.hpp
# Place tl-expected.hpp in appropriate directory
# CMake usage example
find_package(tl-expected REQUIRED)
target_link_libraries(your_target tl::expected)
Basic Error Handling and Validation
#include <tl/expected.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <regex>
// Error type definition
enum class ValidationError {
EMPTY_INPUT,
INVALID_FORMAT,
OUT_OF_RANGE,
INVALID_EMAIL,
INVALID_PHONE,
INVALID_URL
};
// Error message conversion
std::string errorToString(ValidationError error) {
switch (error) {
case ValidationError::EMPTY_INPUT:
return "Input is empty";
case ValidationError::INVALID_FORMAT:
return "Invalid format";
case ValidationError::OUT_OF_RANGE:
return "Value out of range";
case ValidationError::INVALID_EMAIL:
return "Invalid email address";
case ValidationError::INVALID_PHONE:
return "Invalid phone number";
case ValidationError::INVALID_URL:
return "Invalid URL";
default:
return "Unknown error";
}
}
// Basic validation functions
tl::expected<std::string, ValidationError> validateNonEmpty(const std::string& input) {
if (input.empty()) {
return tl::unexpected(ValidationError::EMPTY_INPUT);
}
return input;
}
tl::expected<std::string, ValidationError> validateLength(const std::string& input, size_t min_len, size_t max_len) {
if (input.length() < min_len || input.length() > max_len) {
return tl::unexpected(ValidationError::OUT_OF_RANGE);
}
return input;
}
tl::expected<std::string, ValidationError> validateEmail(const std::string& input) {
static const std::regex email_regex(R"(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)");
if (!std::regex_match(input, email_regex)) {
return tl::unexpected(ValidationError::INVALID_EMAIL);
}
return input;
}
tl::expected<std::string, ValidationError> validatePhone(const std::string& input) {
static const std::regex phone_regex(R"(^(\+81|0)[1-9][0-9]{8,9}$)");
if (!std::regex_match(input, phone_regex)) {
return tl::unexpected(ValidationError::INVALID_PHONE);
}
return input;
}
tl::expected<int, ValidationError> validateAge(const std::string& input) {
try {
int age = std::stoi(input);
if (age < 0 || age > 120) {
return tl::unexpected(ValidationError::OUT_OF_RANGE);
}
return age;
} catch (const std::exception&) {
return tl::unexpected(ValidationError::INVALID_FORMAT);
}
}
// Numeric validation
tl::expected<double, ValidationError> validateNumber(const std::string& input, double min_val, double max_val) {
try {
double value = std::stod(input);
if (value < min_val || value > max_val) {
return tl::unexpected(ValidationError::OUT_OF_RANGE);
}
return value;
} catch (const std::exception&) {
return tl::unexpected(ValidationError::INVALID_FORMAT);
}
}
int main() {
// Basic usage example
{
std::cout << "=== Basic Validation ===" << std::endl;
// Success case
auto result1 = validateNonEmpty("Hello World");
if (result1) {
std::cout << "✓ Success: " << *result1 << std::endl;
} else {
std::cout << "✗ Error: " << errorToString(result1.error()) << std::endl;
}
// Failure case
auto result2 = validateNonEmpty("");
if (result2) {
std::cout << "✓ Success: " << *result2 << std::endl;
} else {
std::cout << "✗ Error: " << errorToString(result2.error()) << std::endl;
}
}
// Sequential validation
{
std::cout << "\n=== Sequential Validation ===" << std::endl;
std::string email = "[email protected]";
// Functional sequential validation
auto result = validateNonEmpty(email)
.and_then([](const std::string& s) { return validateLength(s, 5, 100); })
.and_then([](const std::string& s) { return validateEmail(s); });
if (result) {
std::cout << "✓ Email address is valid: " << *result << std::endl;
} else {
std::cout << "✗ Email address error: " << errorToString(result.error()) << std::endl;
}
}
// Age validation
{
std::cout << "\n=== Age Validation ===" << std::endl;
std::vector<std::string> ages = {"25", "150", "abc", "-5"};
for (const auto& age_str : ages) {
auto result = validateAge(age_str);
if (result) {
std::cout << "✓ Age " << age_str << " is valid: " << *result << " years old" << std::endl;
} else {
std::cout << "✗ Age " << age_str << " is invalid: " << errorToString(result.error()) << std::endl;
}
}
}
// Numeric validation
{
std::cout << "\n=== Numeric Validation ===" << std::endl;
std::vector<std::string> numbers = {"3.14", "100.5", "abc", "200"};
for (const auto& num_str : numbers) {
auto result = validateNumber(num_str, 0.0, 100.0);
if (result) {
std::cout << "✓ Number " << num_str << " is valid: " << *result << std::endl;
} else {
std::cout << "✗ Number " << num_str << " is invalid: " << errorToString(result.error()) << std::endl;
}
}
}
return 0;
}
Advanced Validation Features and Structured Data
#include <tl/expected.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <regex>
// Complex error type
struct ValidationError {
enum Type {
REQUIRED_FIELD_MISSING,
INVALID_TYPE,
INVALID_FORMAT,
OUT_OF_RANGE,
INVALID_REFERENCE,
CUSTOM_ERROR
};
Type type;
std::string field;
std::string message;
ValidationError(Type t, const std::string& f, const std::string& m)
: type(t), field(f), message(m) {}
std::string toString() const {
return "Field '" + field + "': " + message;
}
};
// User information structure
struct UserInfo {
std::string name;
std::string email;
int age;
std::string phone;
std::vector<std::string> skills;
std::map<std::string, std::string> metadata;
UserInfo() = default;
UserInfo(const std::string& n, const std::string& e, int a, const std::string& p)
: name(n), email(e), age(a), phone(p) {}
};
// Advanced validation class
class UserValidator {
private:
// Internal validation functions
static tl::expected<std::string, ValidationError> validateName(const std::string& name) {
if (name.empty()) {
return tl::unexpected(ValidationError(ValidationError::REQUIRED_FIELD_MISSING,
"name", "Name is required"));
}
if (name.length() < 2 || name.length() > 50) {
return tl::unexpected(ValidationError(ValidationError::OUT_OF_RANGE,
"name", "Name must be between 2 and 50 characters"));
}
return name;
}
static tl::expected<std::string, ValidationError> validateEmailFormat(const std::string& email) {
if (email.empty()) {
return tl::unexpected(ValidationError(ValidationError::REQUIRED_FIELD_MISSING,
"email", "Email address is required"));
}
static const std::regex email_regex(R"(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)");
if (!std::regex_match(email, email_regex)) {
return tl::unexpected(ValidationError(ValidationError::INVALID_FORMAT,
"email", "Invalid email address format"));
}
return email;
}
static tl::expected<int, ValidationError> validateAgeRange(int age) {
if (age < 0 || age > 120) {
return tl::unexpected(ValidationError(ValidationError::OUT_OF_RANGE,
"age", "Age must be between 0 and 120"));
}
return age;
}
static tl::expected<std::string, ValidationError> validatePhoneFormat(const std::string& phone) {
if (phone.empty()) {
return phone; // Phone number is optional
}
static const std::regex phone_regex(R"(^(\+81|0)[1-9][0-9]{8,9}$)");
if (!std::regex_match(phone, phone_regex)) {
return tl::unexpected(ValidationError(ValidationError::INVALID_FORMAT,
"phone", "Invalid phone number format"));
}
return phone;
}
static tl::expected<std::vector<std::string>, ValidationError> validateSkills(const std::vector<std::string>& skills) {
if (skills.size() > 20) {
return tl::unexpected(ValidationError(ValidationError::OUT_OF_RANGE,
"skills", "Maximum of 20 skills allowed"));
}
for (const auto& skill : skills) {
if (skill.empty()) {
return tl::unexpected(ValidationError(ValidationError::INVALID_FORMAT,
"skills", "Empty skills are not allowed"));
}
if (skill.length() > 30) {
return tl::unexpected(ValidationError(ValidationError::OUT_OF_RANGE,
"skills", "Each skill must be 30 characters or less"));
}
}
return skills;
}
public:
// Individual field validation
static tl::expected<std::string, ValidationError> validateUserName(const std::string& name) {
return validateName(name);
}
static tl::expected<std::string, ValidationError> validateUserEmail(const std::string& email) {
return validateEmailFormat(email);
}
static tl::expected<int, ValidationError> validateUserAge(int age) {
return validateAgeRange(age);
}
static tl::expected<std::string, ValidationError> validateUserPhone(const std::string& phone) {
return validatePhoneFormat(phone);
}
// Composite validation
static tl::expected<UserInfo, ValidationError> validateUser(const UserInfo& user) {
// Name validation
auto name_result = validateName(user.name);
if (!name_result) {
return tl::unexpected(name_result.error());
}
// Email validation
auto email_result = validateEmailFormat(user.email);
if (!email_result) {
return tl::unexpected(email_result.error());
}
// Age validation
auto age_result = validateAgeRange(user.age);
if (!age_result) {
return tl::unexpected(age_result.error());
}
// Phone validation
auto phone_result = validatePhoneFormat(user.phone);
if (!phone_result) {
return tl::unexpected(phone_result.error());
}
// Skills validation
auto skills_result = validateSkills(user.skills);
if (!skills_result) {
return tl::unexpected(skills_result.error());
}
// All successful
UserInfo validated_user = user;
validated_user.name = *name_result;
validated_user.email = *email_result;
validated_user.age = *age_result;
validated_user.phone = *phone_result;
validated_user.skills = *skills_result;
return validated_user;
}
// Batch validation
static std::vector<tl::expected<UserInfo, ValidationError>> validateUsers(const std::vector<UserInfo>& users) {
std::vector<tl::expected<UserInfo, ValidationError>> results;
results.reserve(users.size());
for (const auto& user : users) {
results.push_back(validateUser(user));
}
return results;
}
};
// Conditional validation
class ConditionalValidator {
public:
// Age-based conditional validation
static tl::expected<UserInfo, ValidationError> validateUserWithConditions(const UserInfo& user) {
// Basic validation
auto basic_result = UserValidator::validateUser(user);
if (!basic_result) {
return basic_result;
}
UserInfo validated_user = *basic_result;
// Conditional validation
if (validated_user.age < 18) {
// Phone number required for minors
if (validated_user.phone.empty()) {
return tl::unexpected(ValidationError(ValidationError::REQUIRED_FIELD_MISSING,
"phone", "Phone number required for minors"));
}
}
if (validated_user.age >= 65) {
// Special validation for seniors
if (validated_user.skills.empty()) {
return tl::unexpected(ValidationError(ValidationError::CUSTOM_ERROR,
"skills", "Please provide skills information"));
}
}
return validated_user;
}
};
// Actual usage example
int main() {
try {
// Test valid user information
{
std::cout << "=== Valid User Information Test ===" << std::endl;
UserInfo user("John Doe", "[email protected]", 30, "+1234567890");
user.skills = {"C++", "Python", "JavaScript"};
user.metadata = {{"department", "Engineering"}, {"level", "Senior"}};
auto result = UserValidator::validateUser(user);
if (result) {
std::cout << "✓ User information is valid:" << std::endl;
std::cout << " Name: " << result->name << std::endl;
std::cout << " Email: " << result->email << std::endl;
std::cout << " Age: " << result->age << std::endl;
std::cout << " Phone: " << result->phone << std::endl;
std::cout << " Skills count: " << result->skills.size() << std::endl;
} else {
std::cout << "✗ " << result.error().toString() << std::endl;
}
}
// Test invalid user information
{
std::cout << "\n=== Invalid User Information Test ===" << std::endl;
std::vector<UserInfo> invalid_users = {
{"", "[email protected]", 30, "+1234567890"}, // Empty name
{"John Doe", "invalid-email", 30, "+1234567890"}, // Invalid email
{"John Doe", "[email protected]", 150, "+1234567890"}, // Out of range age
{"John Doe", "[email protected]", 30, "123"}, // Invalid phone number
};
auto results = UserValidator::validateUsers(invalid_users);
for (size_t i = 0; i < results.size(); ++i) {
std::cout << "User " << (i + 1) << ": ";
if (results[i]) {
std::cout << "✓ Valid" << std::endl;
} else {
std::cout << "✗ " << results[i].error().toString() << std::endl;
}
}
}
// Conditional validation
{
std::cout << "\n=== Conditional Validation ===" << std::endl;
// Minor user (no phone number)
UserInfo minor("Student John", "[email protected]", 16, "");
minor.skills = {"Study", "Sports"};
auto result = ConditionalValidator::validateUserWithConditions(minor);
if (result) {
std::cout << "✓ Minor user is valid" << std::endl;
} else {
std::cout << "✗ Minor user: " << result.error().toString() << std::endl;
}
// Senior user (no skills)
UserInfo senior("Senior Jane", "[email protected]", 70, "+1987654321");
auto result2 = ConditionalValidator::validateUserWithConditions(senior);
if (result2) {
std::cout << "✓ Senior user is valid" << std::endl;
} else {
std::cout << "✗ Senior user: " << result2.error().toString() << std::endl;
}
}
// Functional style sequential validation
{
std::cout << "\n=== Functional Style Sequential Validation ===" << std::endl;
std::string name = "Jane Smith";
std::string email = "[email protected]";
int age = 25;
std::string phone = "+1555123456";
auto final_result = UserValidator::validateUserName(name)
.and_then([&](const std::string& validated_name) {
return UserValidator::validateUserEmail(email)
.and_then([&](const std::string& validated_email) {
return UserValidator::validateUserAge(age)
.and_then([&](int validated_age) {
return UserValidator::validateUserPhone(phone)
.map([&](const std::string& validated_phone) {
return UserInfo(validated_name, validated_email, validated_age, validated_phone);
});
});
});
});
if (final_result) {
std::cout << "✓ All validations successful" << std::endl;
std::cout << " Created user: " << final_result->name << " (" << final_result->email << ")" << std::endl;
} else {
std::cout << "✗ Validation error: " << final_result.error().toString() << std::endl;
}
}
} catch (const std::exception& e) {
std::cerr << "Unexpected error: " << e.what() << std::endl;
}
return 0;
}
Performance Comparison and Benchmarks
#include <tl/expected.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <chrono>
#include <stdexcept>
#include <random>
// Data structure for validation
struct TestData {
std::string value;
int number;
TestData(const std::string& v, int n) : value(v), number(n) {}
};
// Exception-based validation
class ExceptionValidator {
public:
static TestData validateWithExceptions(const TestData& data) {
if (data.value.empty()) {
throw std::invalid_argument("Value is empty");
}
if (data.value.length() < 5 || data.value.length() > 50) {
throw std::invalid_argument("Invalid value length");
}
if (data.number < 0 || data.number > 1000) {
throw std::invalid_argument("Number out of range");
}
return data;
}
};
// tl::expected based validation
class ExpectedValidator {
public:
enum class Error {
EMPTY_VALUE,
INVALID_LENGTH,
OUT_OF_RANGE
};
static tl::expected<TestData, Error> validateWithExpected(const TestData& data) {
if (data.value.empty()) {
return tl::unexpected(Error::EMPTY_VALUE);
}
if (data.value.length() < 5 || data.value.length() > 50) {
return tl::unexpected(Error::INVALID_LENGTH);
}
if (data.number < 0 || data.number > 1000) {
return tl::unexpected(Error::OUT_OF_RANGE);
}
return data;
}
};
// Test data generation
class TestDataGenerator {
private:
std::random_device rd;
std::mt19937 gen{rd()};
std::uniform_int_distribution<> length_dist{1, 60};
std::uniform_int_distribution<> char_dist{'a', 'z'};
std::uniform_int_distribution<> number_dist{-100, 1100};
std::uniform_real_distribution<> valid_ratio{0.0, 1.0};
public:
std::vector<TestData> generateTestData(size_t count, double valid_percentage = 0.7) {
std::vector<TestData> data;
data.reserve(count);
for (size_t i = 0; i < count; ++i) {
bool should_be_valid = valid_ratio(gen) < valid_percentage;
// String generation
std::string value;
if (should_be_valid) {
int length = std::uniform_int_distribution<>{5, 50}(gen);
value.reserve(length);
for (int j = 0; j < length; ++j) {
value += static_cast<char>(char_dist(gen));
}
} else {
// Generate invalid data
if (valid_ratio(gen) < 0.3) {
value = ""; // Empty string
} else {
int length = std::uniform_int_distribution<>{1, 4}(gen); // Too short
value.reserve(length);
for (int j = 0; j < length; ++j) {
value += static_cast<char>(char_dist(gen));
}
}
}
// Number generation
int number;
if (should_be_valid) {
number = std::uniform_int_distribution<>{0, 1000}(gen);
} else {
number = number_dist(gen); // Potentially out of range
}
data.emplace_back(value, number);
}
return data;
}
};
// Performance test
class PerformanceTest {
public:
struct BenchmarkResult {
size_t total_items;
size_t valid_items;
size_t invalid_items;
double elapsed_ms;
double items_per_second;
void print(const std::string& name) const {
std::cout << name << " Results:" << std::endl;
std::cout << " Total items: " << total_items << std::endl;
std::cout << " Valid items: " << valid_items << std::endl;
std::cout << " Invalid items: " << invalid_items << std::endl;
std::cout << " Processing time: " << elapsed_ms << " ms" << std::endl;
std::cout << " Processing speed: " << items_per_second << " items/sec" << std::endl;
}
};
static BenchmarkResult testExceptionBased(const std::vector<TestData>& test_data) {
BenchmarkResult result;
result.total_items = test_data.size();
result.valid_items = 0;
result.invalid_items = 0;
auto start = std::chrono::high_resolution_clock::now();
for (const auto& data : test_data) {
try {
ExceptionValidator::validateWithExceptions(data);
result.valid_items++;
} catch (const std::exception&) {
result.invalid_items++;
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
result.elapsed_ms = duration.count() / 1000.0;
result.items_per_second = (result.total_items * 1000000.0) / duration.count();
return result;
}
static BenchmarkResult testExpectedBased(const std::vector<TestData>& test_data) {
BenchmarkResult result;
result.total_items = test_data.size();
result.valid_items = 0;
result.invalid_items = 0;
auto start = std::chrono::high_resolution_clock::now();
for (const auto& data : test_data) {
auto validation_result = ExpectedValidator::validateWithExpected(data);
if (validation_result) {
result.valid_items++;
} else {
result.invalid_items++;
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
result.elapsed_ms = duration.count() / 1000.0;
result.items_per_second = (result.total_items * 1000000.0) / duration.count();
return result;
}
};
// Functional usage example
class FunctionalExample {
public:
// Combine multiple validations
static tl::expected<TestData, std::string> validateWithMultipleSteps(const TestData& data) {
return ExpectedValidator::validateWithExpected(data)
.map_error([](ExpectedValidator::Error error) {
switch (error) {
case ExpectedValidator::Error::EMPTY_VALUE:
return std::string("Value is empty");
case ExpectedValidator::Error::INVALID_LENGTH:
return std::string("Invalid value length");
case ExpectedValidator::Error::OUT_OF_RANGE:
return std::string("Number out of range");
default:
return std::string("Unknown error");
}
})
.and_then([](const TestData& validated_data) -> tl::expected<TestData, std::string> {
// Additional validation logic
if (validated_data.value.find("invalid") != std::string::npos) {
return tl::unexpected(std::string("Contains invalid string"));
}
return validated_data;
});
}
// Batch processing
static std::vector<tl::expected<TestData, std::string>> validateBatch(const std::vector<TestData>& data) {
std::vector<tl::expected<TestData, std::string>> results;
results.reserve(data.size());
for (const auto& item : data) {
results.push_back(validateWithMultipleSteps(item));
}
return results;
}
// Extract only valid data
static std::vector<TestData> extractValidData(const std::vector<TestData>& data) {
std::vector<TestData> valid_data;
for (const auto& item : data) {
auto result = validateWithMultipleSteps(item);
if (result) {
valid_data.push_back(*result);
}
}
return valid_data;
}
};
int main() {
try {
TestDataGenerator generator;
// Small-scale test
std::cout << "=== Small-scale Performance Test (10,000 items) ===" << std::endl;
auto small_data = generator.generateTestData(10000, 0.7);
auto exception_result = PerformanceTest::testExceptionBased(small_data);
exception_result.print("Exception-based");
std::cout << std::endl;
auto expected_result = PerformanceTest::testExpectedBased(small_data);
expected_result.print("tl::expected-based");
double speedup = expected_result.items_per_second / exception_result.items_per_second;
std::cout << "\nSpeedup: " << speedup << "x" << std::endl;
// Large-scale test
std::cout << "\n=== Large-scale Performance Test (100,000 items) ===" << std::endl;
auto large_data = generator.generateTestData(100000, 0.7);
auto large_exception_result = PerformanceTest::testExceptionBased(large_data);
large_exception_result.print("Exception-based");
std::cout << std::endl;
auto large_expected_result = PerformanceTest::testExpectedBased(large_data);
large_expected_result.print("tl::expected-based");
double large_speedup = large_expected_result.items_per_second / large_exception_result.items_per_second;
std::cout << "\nSpeedup: " << large_speedup << "x" << std::endl;
// Functional usage example
std::cout << "\n=== Functional Usage Example ===" << std::endl;
auto sample_data = generator.generateTestData(10, 0.5);
auto functional_results = FunctionalExample::validateBatch(sample_data);
std::cout << "Batch processing results:" << std::endl;
for (size_t i = 0; i < functional_results.size(); ++i) {
const auto& result = functional_results[i];
std::cout << " Item " << (i + 1) << ": ";
if (result) {
std::cout << "✓ Valid (value: " << result->value.substr(0, 10) << "..., number: " << result->number << ")" << std::endl;
} else {
std::cout << "✗ Error: " << result.error() << std::endl;
}
}
// Extract only valid data
auto valid_data = FunctionalExample::extractValidData(sample_data);
std::cout << "\nValid data extracted: " << valid_data.size() << " / " << sample_data.size() << " items" << std::endl;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}