COMS W4995 C++ Deep Dive for C Programmers

Index of 2025-9/code/16

Parent directory
Makefile
fwd-ref.cpp
values.cpp
variadic.cpp

Makefile

CC  = g++
CXX = g++

CFLAGS   = -g -Wall
CXXFLAGS = -g -Wall -std=c++20

# turn off warnings about unused variables
CXXFLAGS += -Wno-unused-variable

executables = values fwd-ref variadic

.PHONY: default
default: $(executables)

values: values.o

fwd-ref: fwd-ref.o

variadic: variadic.o

.PHONY: clean
clean:
	rm -f *~ a.out core *.o $(executables)

.PHONY: all
all: clean default

fwd-ref.cpp

#include <string>
#include <iostream>
#include <iomanip>
#include <utility>
using namespace std;

// Wrapper for heap-allocated double
struct D {
    D(double x = -1.0) : p{new double(x)} { cout << "(ctor:" << *p << ") "; }
    ~D() { cout << "(dtor:" << (p ? *p : 0) << ") "; delete p; }

    D(const D& d) : p{new double(*d.p)} { cout << "(copy:" << *p << ") "; }
    D(D&& d) : p{d.p} { d.p = nullptr; cout << "(move:" << *p << ") "; }

    D& operator=(const D& d) = delete;
    D& operator=(D&& d) = delete;

    operator double&() { return *p; }
    operator const double&() const { return *p; }

    double* p;
};

//////////////////////////////////////////////////////////////////////
// Forwarding reference
//

namespace c2cpp {
    template<typename T> struct remove_reference { using type = T; };
    template<typename T> struct remove_reference<T&> { using type = T; };
    template<typename T> struct remove_reference<T&&> { using type = T; };

    template<typename T>
    using remove_reference_t = typename remove_reference<T>::type;

    template<typename T>
    T&& forward(remove_reference_t<T>& t) noexcept {
        return static_cast<T&&>(t);
    }
}

template <typename T> void f1(T t)   { t += 0.1; }
template <typename T> void f2(T& t)  { t += 0.2; }
template <typename T> void f3(T&& t) { t += 0.3; }
template <typename T> void f4(T&& t) { D d{t}; d += 0.4; }
template <typename T> void f5(T&& t) { D d{std::forward<T>(t)}; d += 0.5; }
template <typename T> void f6(T&& t) { D d{c2cpp::forward<T>(t)}; d += 0.6; }

int main() {

    // 1. f1: we can pass lvalue or rvalue but t is a copy
    cout << "\ntemplate <typename T> void f1(T t) { t += 0.1; }\n";
    {
        D d1 {1.0};  
        f1(d1);  
        f1(D(2.0));  
    }
    cout << '\n';

    // 2. f2: cannot pass rvalue
    cout << "\ntemplate <typename T> void f2(T& t) { t += 0.2; }\n";
    {
        D d1 {1.0};  
        f2(d1);  
        // f2(D(2.0)); // err: cannot bind rvalue to T&
    }
    cout << '\n';  

    // 3. f3: forwarding reference
    cout << "\ntemplate <typename T> void f3(T&& t) { t += 0.3; }\n";
    {
        D d1 {1.0};
        f3(d1);     // lvalue arg: T = D&, so T&& is D&
        f3(D(2.0)); // rvalue arg: T = D,  so T&& is D&&
    }
    cout << '\n';

    // 4. f4: a forwarding reference is always an lvalue
    cout << "\ntemplate <typename T> void f4(T&& t) { D d{t}; d += 0.4; }\n";
    {
        D d1 {1.0};
        f4(d1);     // t is lvalue inside f4
        f4(D(2.0)); // t is also lvalue inside f4
    }
    cout << '\n';

    // 5. f5: perfect-forwarding a forwarding reference
    cout << "\ntemplate <typename T> "
              "void f5(T&& t) { D d{std::forward<T>(t)}; d += 0.5; }\n";
    {
        D d1 {1.0};
        f5(d1);
        f5(D(2.0));
    }
    cout << '\n';

    // 6. f6: forward<T> implementation
    cout << "\ntemplate <typename T> "
              "void f6(T&& t) { D d{c2cpp::forward<T>(t)}; d += 0.6; }\n";
    {
        D d1 {1.0};
        f6(d1);
        f6(D(2.0));
    }
    cout << '\n';
}

values.cpp

#include <string>
#include <iostream>
#include <iomanip>
#include <utility>
using namespace std;

// Wrapper for heap-allocated double
struct D {
    D(double x = -1.0) : p{new double(x)} { cout << "(ctor:" << *p << ") "; }
    ~D() { cout << "(dtor:" << (p ? *p : 0) << ") "; delete p; }

    D(const D& d) : p{new double(*d.p)} { cout << "(copy:" << *p << ") "; }
    D(D&& d) : p{d.p} { d.p = nullptr; cout << "(move:" << *p << ") "; }

    D& operator=(const D& d) = delete;
    D& operator=(D&& d) = delete;

    operator double&() { return *p; }
    operator const double&() const { return *p; }

    double* p;
};

int main() {
    //////////////////////////////////////////////////////////////////
    // 1. Binding lvalue
    //

    D d1 { 1.0 };
    D& rd1 = d1;
    const D& crd1 = d1;
    // D&& rrd1 = d1; // err: cannot bind lvalue to rvalue reference

    //////////////////////////////////////////////////////////////////
    // 2. Binding rvalue
    //

    // D& rd2 = D(2.0); // err: cannot bind rvalue to lvalue reference
    const D& crd3 = D(3.0);
    D&& rrd4 = D(4.0);
    // the temp object is mutable through rvalue ref
    rrd4 += 0.1; 
    // and both temp objects are still alive!
    cout << "[" << crd3 << "," << rrd4 << "] ";

    //////////////////////////////////////////////////////////////////
    // 3. Binding rvalue reference to another rvalue reference
    //

    // D&& rrd4_1 = rrd4; // err: rvalue reference itself is an lvalue!

    D&  rrd4_2 = rrd4; // ok: rvalue reference itself is an lvalue!
    D&& rrd4_3 = std::move(rrd4); // ok: cast rrd4 back to rvalue again

    //////////////////////////////////////////////////////////////////
    // 4. Summary of lvalue & rvalue
    //
    // * lvalue
    //     - named variables, function call returning by reference, etc.
    //     - ex) i, s[i]  (assuming i is int, and s is std::string)
    //
    // * rvalue
    //     - literals, function call returning by value, unnamed temp object,
    //       cast expression to rvalue reference, etc.
    //     - two sub-categories:
    //         1) prvalues - ex) 2.0, s + s, string("abc")
    //         2) xvalues  - ex) std::move(s)
    //
    // Some surprises:
    //     - glvalue (generalized lvalue) is the union of lvalue and xvalue
    //     - rvalue reference itself is an lvalue because it has a name!
    //     - C string literals (ex. "abc") are lvalues
    //

    cout << "(bye) ";
}

variadic.cpp

#include <string>
#include <iostream>
#include <iomanip>
#include <utility>
using namespace std;

// Wrapper for heap-allocated double
struct D {
    D(double x = -1.0) : p{new double(x)} { cout << "(ctor:" << *p << ") "; }
    ~D() { cout << "(dtor:" << (p ? *p : 0) << ") "; delete p; }

    D(const D& d) : p{new double(*d.p)} { cout << "(copy:" << *p << ") "; }
    D(D&& d) : p{d.p} { d.p = nullptr; cout << "(move:" << *p << ") "; }

    D& operator=(const D& d) = delete;
    D& operator=(D&& d) = delete;

    operator double&() { return *p; }
    operator const double&() const { return *p; }

    double* p;
};

// 1. Basic variadic template using template recursion

void print_v1() { cout << '\n'; } // base case for template recursion

template <typename T, typename... MoreTs>    // template parameter pack
void print_v1(T arg0, MoreTs... more_args) { // function parameter pack
    cout << arg0 << ' ';
    // pack expansion of a pattern containing function parameter pack
    print_v1(more_args...);
}

// 2. Function parameters as forwarding references to reduce copying

void print_v2() { cout << '\n'; }

template <typename T, typename... MoreTs>
void print_v2(T&& arg0, MoreTs&&... more_args) { // forwarding reference
    cout << arg0 << ' ';
    // use std::forward on each arg in the parameter pack
    print_v2(std::forward<MoreTs>(more_args)...);
}

// 3. Fold expression

template <typename... Types>
void print_v3(Types&&... args) {
    // Binary left fold without printing spaces
    (cout << ... << args) << '\n';

    // Unary right fold with comma operator to print spaces
    ((cout << args << ' '), ...) << '\n';
}

int main() {
    string s { "Hi" };

    print_v1(s, "ABC", 45, string{"xyz"});
    cout << '\n';
    print_v1(s, "ABC", 45, string{"xyz"}, D{3.14});

    // print_v2(s, "ABC", 45, string{"xyz"}, D{3.14});

    // print_v3(s, "ABC", 45, string{"xyz"}, D{3.14});
}