Menu
Coddy logo textTech

Validation in Setters

Part of the Object Oriented Programming section of Coddy's C journey — lesson 20 of 61.

In the previous lesson, we created basic setters that simply assign values. But this misses a key benefit of encapsulation: the ability to protect your data by adding validation logic.

Consider a Counter that should never go negative. Without encapsulation, anyone could write c->value = -50 and corrupt the state. With a setter, you control what values are allowed:

// counter.c
void counter_set_value(Counter *c, int new_value) {
    if (new_value < 0) {
        return;  // Reject invalid input silently
    }
    c->value = new_value;
}

Now negative values are simply ignored.

You might also choose to return a status code to indicate success or failure:

int counter_set_value(Counter *c, int new_value) {
    if (new_value < 0) {
        return 0;  // Failure
    }
    c->value = new_value;
    return 1;  // Success
}

This pattern lets the caller know whether the operation succeeded. The key insight is that setters aren't just about assignment—they're gatekeepers that enforce rules about what your object's state can be. This is the real power of encapsulation: your object maintains its own integrity, regardless of what external code tries to do.

challenge icon

Challenge

Easy

Let's build a Temperature module that protects its data using validation in the setter. You'll create a temperature tracker that only accepts values within a valid range—demonstrating how setters act as gatekeepers for your object's state.

You'll create three files:

  • temperature.h: Your public interface using the opaque pointer pattern. Declare a Temperature type with a forward declaration only. Declare these functions:
    • create_temperature — takes an integer for initial degrees and returns a Temperature pointer
    • free_temperature — releases the allocated memory
    • temp_get_degrees — getter that returns the current temperature value
    • temp_set_degrees — setter that returns an int (1 for success, 0 for failure)
    Use include guards with the symbol TEMPERATURE_H.
  • temperature.c: Define the actual struct Temperature with a hidden int degrees member. Implement all functions. The key is your setter—it should only accept values between -50 and 150 (inclusive). If the value is outside this range, return 0 and leave the temperature unchanged. If valid, update the value and return 1.
  • main.c: Demonstrate the validation by attempting to set both valid and invalid temperatures, showing that invalid values are rejected while valid ones are accepted.

You will receive three inputs: the initial temperature, a valid temperature to set (within range), and an invalid temperature to attempt (outside range).

In your main file, create a Temperature with the initial value and print it. Then attempt to set the invalid temperature—since it should be rejected, print the result and show that the value remains unchanged. Finally, set the valid temperature, print the success result, and show the updated value. Free the memory and confirm cleanup.

Print the output in this format:

Initial: {degrees}
Set {invalid}: {0 or 1}
After invalid: {degrees}
Set {valid}: {0 or 1}
After valid: {degrees}
Freed

For example, with inputs 25, 100, and 200, the output would be:

Initial: 25
Set 200: 0
After invalid: 25
Set 100: 1
After valid: 100
Freed

This demonstrates the real power of encapsulation—your setter enforces the rules about what constitutes a valid temperature, protecting the object's integrity regardless of what values external code tries to assign.

Cheat sheet

Setters can include validation logic to protect data integrity. Instead of simply assigning values, they act as gatekeepers that enforce rules about valid object states.

Basic validation example - rejecting invalid input silently:

void counter_set_value(Counter *c, int new_value) {
    if (new_value < 0) {
        return;  // Reject invalid input
    }
    c->value = new_value;
}

Returning a status code to indicate success or failure:

int counter_set_value(Counter *c, int new_value) {
    if (new_value < 0) {
        return 0;  // Failure
    }
    c->value = new_value;
    return 1;  // Success
}

This pattern allows callers to check whether the operation succeeded, enabling objects to maintain their own integrity regardless of external code attempts.

Try it yourself

#include <stdio.h>
#include "temperature.h"

int main() {
    // Read inputs
    int initial, valid_temp, invalid_temp;
    scanf("%d", &initial);
    scanf("%d", &valid_temp);
    scanf("%d", &invalid_temp);
    
    // TODO: Create a Temperature with the initial value
    
    // TODO: Print initial temperature
    // Format: "Initial: {degrees}"
    
    // TODO: Attempt to set the invalid temperature
    // Print the result and show value remains unchanged
    // Format: "Set {invalid}: {0 or 1}"
    // Format: "After invalid: {degrees}"
    
    // TODO: Set the valid temperature
    // Print the success result and show updated value
    // Format: "Set {valid}: {0 or 1}"
    // Format: "After valid: {degrees}"
    
    // TODO: Free the memory and print "Freed"
    
    return 0;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming