How to Use Protocol Buffers in C++

A practical guide to working with Protocol Buffers in C++ - the original implementation

Published: January 2025 • 10 min read

Protocol Buffers was originally developed by Google for C++, making it the most mature and optimized implementation. If you need the absolute best performance for serialization, C++ with Protobuf is hard to beat. It's used in high-frequency trading, game servers, telecommunications infrastructure, and anywhere microseconds matter.

This guide walks you through using Protocol Buffers in C++. We'll use simple examples while covering the powerful features that make C++ protobuf the gold standard for performance-critical applications.

What You'll Need

  • C++ Compiler: GCC 7+, Clang 6+, or MSVC 2017+
  • CMake: Version 3.15+ for building
  • Protocol Buffer Compiler: We'll install this next

Step 1: Install Protocol Buffers

On Ubuntu/Debian:

sudo apt update
sudo apt install -y protobuf-compiler libprotobuf-dev

# Verify installation
protoc --version

On macOS with Homebrew:

brew install protobuf

On Windows, download from GitHub releases. For vcpkg users:

vcpkg install protobuf protobuf:x64-windows

Step 2: Set Up Your Project

Create a C++ project structure:

mkdir cpp-protobuf-demo
cd cpp-protobuf-demo
mkdir protos src build

Create a CMakeLists.txt:

cmake_minimum_required(VERSION 3.15)
project(ProtobufDemo)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Find Protobuf
find_package(Protobuf REQUIRED)

# Generate protobuf files
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS protos/subscriber.proto)

# Include directories
include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(${Protobuf_INCLUDE_DIRS})

# Create executable
add_executable(demo src/main.cpp ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(demo ${Protobuf_LIBRARIES})

Step 3: Create Your .proto File

Create protos/subscriber.proto:

syntax = "proto3";

package telecom;

// Mobile subscriber information
message Subscriber {
  string msisdn = 1;           // Mobile number
  string name = 2;             // Subscriber name
  string email = 3;            // Email address
  SubscriptionType type = 4;   // Plan type
  bool active = 5;             // Account status
  repeated string services = 6; // Active services
}

enum SubscriptionType {
  PREPAID = 0;
  POSTPAID = 1;
  CORPORATE = 2;
}

Step 4: Use Protocol Buffers in C++

Create src/main.cpp:

#include <iostream>
#include <fstream>
#include "subscriber.pb.h"

int main() {
    // Verify that the version of the library we linked against is
    // compatible with the version of the headers we compiled against
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    // Create a new subscriber
    telecom::Subscriber subscriber;
    subscriber.set_msisdn("+91-9876543210");
    subscriber.set_name("Telecom Customer");
    subscriber.set_email("customer@telecom.com");
    subscriber.set_type(telecom::SubscriptionType::POSTPAID);
    subscriber.set_active(true);
    subscriber.add_services("Voice");
    subscriber.add_services("Data");
    subscriber.add_services("4G LTE");

    std::cout << "Created subscriber: " << subscriber.name() << std::endl;
    std::cout << "MSISDN: " << subscriber.msisdn() << std::endl;
    std::cout << "Type: " << subscriber.type() << std::endl;

    // Serialize to binary
    std::string binary_data;
    subscriber.SerializeToString(&binary_data);
    std::cout << "Serialized to " << binary_data.size() << " bytes\n" << std::endl;

    // Deserialize from binary
    telecom::Subscriber decoded;
    decoded.ParseFromString(binary_data);
    
    std::cout << "Deserialized subscriber: " << decoded.name() << std::endl;
    std::cout << "Email: " << decoded.email() << std::endl;
    std::cout << "Active: " << (decoded.active() ? "Yes" : "No") << std::endl;

    // Access repeated fields
    std::cout << "\nActive services:" << std::endl;
    for (int i = 0; i < decoded.services_size(); i++) {
        std::cout << "  - " << decoded.services(i) << std::endl;
    }

    // Clean up
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

Step 5: Build and Run

Build the project:

cd build
cmake ..
make

# Run
./demo

You should see output like:

Created subscriber: Telecom Customer
MSISDN: +91-9876543210
Type: 1
Serialized to 68 bytes

Deserialized subscriber: Telecom Customer
Email: customer@telecom.com
Active: Yes

Active services:
  - Voice
  - Data
  - 4G LTE

Saving and Loading from Files

Save and load protobuf data to/from files:

#include <fstream>
#include "subscriber.pb.h"

void saveToFile() {
    telecom::Subscriber subscriber;
    subscriber.set_msisdn("+91-9123456789");
    subscriber.set_name("Network Admin");
    subscriber.set_email("admin@telecom.com");
    subscriber.set_type(telecom::SubscriptionType::CORPORATE);
    subscriber.set_active(true);

    // Save to file
    std::fstream output("subscriber.bin", 
                        std::ios::out | std::ios::binary);
    if (!subscriber.SerializeToOstream(&output)) {
        std::cerr << "Failed to write subscriber" << std::endl;
        return;
    }
    output.close();
    std::cout << "Saved to file" << std::endl;
}

void loadFromFile() {
    telecom::Subscriber subscriber;
    
    // Load from file
    std::fstream input("subscriber.bin", 
                       std::ios::in | std::ios::binary);
    if (!subscriber.ParseFromIstream(&input)) {
        std::cerr << "Failed to parse subscriber" << std::endl;
        return;
    }
    input.close();
    
    std::cout << "Loaded: " << subscriber.name() << std::endl;
    std::cout << "MSISDN: " << subscriber.msisdn() << std::endl;
}

Convert Between JSON and Protobuf

C++ protobuf supports JSON conversion (requires protobuf 3.0+):

#include <google/protobuf/util/json_util.h>
#include "subscriber.pb.h"

void jsonExample() {
    telecom::Subscriber subscriber;
    subscriber.set_msisdn("+91-9876543210");
    subscriber.set_name("Mobile User");
    subscriber.set_type(telecom::SubscriptionType::PREPAID);
    subscriber.set_active(true);

    // Convert to JSON
    std::string json_string;
    google::protobuf::util::MessageToJsonString(subscriber, &json_string);
    std::cout << "JSON output:" << std::endl;
    std::cout << json_string << std::endl;

    // Parse from JSON
    std::string json_data = R"({
        "msisdn": "+91-9111111111",
        "name": "New Subscriber",
        "type": "POSTPAID",
        "active": true
    })";

    telecom::Subscriber parsed;
    google::protobuf::util::JsonStringToMessage(json_data, &parsed);
    std::cout << "\nParsed from JSON: " << parsed.name() << std::endl;
    std::cout << "Type: " << parsed.type() << std::endl;
}

High-Performance Arena Allocation

For maximum performance, use Arena allocation to reduce memory allocations:

#include <google/protobuf/arena.h>
#include "subscriber.pb.h"

void arenaExample() {
    // Create arena
    google::protobuf::Arena arena;

    // Allocate messages on arena (no manual delete needed)
    telecom::Subscriber* subscriber = 
        google::protobuf::Arena::CreateMessage<telecom::Subscriber>(&arena);
    
    subscriber->set_msisdn("+91-9876543210");
    subscriber->set_name("Arena User");
    subscriber->set_type(telecom::SubscriptionType::POSTPAID);
    subscriber->add_services("Voice");
    subscriber->add_services("Data");

    std::cout << "Created: " << subscriber->name() << std::endl;
    std::cout << "Services: " << subscriber->services_size() << std::endl;

    // Arena automatically cleans up all messages
}

Performance Tip: Arena allocation can be 2-5x faster for workloads with many message allocations because it eliminates individual deallocations.

Best Practices for C++

Use Arena Allocation

For high-performance scenarios with many messages, arena allocation dramatically reduces allocation overhead and improves cache locality.

Reuse Message Objects

Call Clear() on messages instead of creating new ones repeatedly. This reuses allocated memory.

Use Binary Format

The binary format is 3-10x faster than JSON. Only use JSON for debugging or API compatibility.

Check Return Values

Always check return values from ParseFromString() andSerializeToString() to handle errors.

Call ShutdownProtobufLibrary()

Always call this before your program exits to free global resources and avoid memory leak warnings.

Common Issues

Issue: Linker errors with protobuf library

Solution: Make sure you're linking against the correct protobuf library. Use find_package(Protobuf REQUIRED) in CMake and link with ${Protobuf_LIBRARIES}.

Issue: Version mismatch warnings

Solution: Make sure the protoc compiler version matches your libprotobuf version. Check with protoc --version.

Issue: Parse errors with binary data

Solution: Always check return values from Parse methods. Use binary mode (std::ios::binary) when working with files.

Related Tools

Additional Resources

Official Documentation & References

Conclusion

Protocol Buffers in C++ delivers unmatched performance for serialization. As the original implementation, it's the most mature, optimized, and feature-complete. If your application needs the absolute best performance, C++ with protobuf is the way to go.

Start with these examples and explore advanced features like arena allocation, zero-copy parsing, and custom allocators. The performance benefits make protobuf essential for high-throughput, low-latency C++ applications.