Circa 1995, Microsoft generalized input devices. This enabled the retirement of analog joysticks and their driver cards and gave way to the USB digital variants. This support is provided in the DirectX library, more specifically as the Direct Input API.
Amazingly, Microsoft has preserved backwards compatibility through Windows 10, but there are a few particularities:

1. "Due to the transition of DirectX headers and libraries into the Windows SDK, changes to the project settings are needed to build these samples correctly with how the Windows 8 SDK and later is packaged with the premium Visual Studio SKUs." [1]

[1] https://docs.microsoft.com/en-us/windows/win32/directx-sdk--august-2009-

In More human temrs: there are several places that the DirectX libraries can reside.Thus you need to specify which directory you want the program to exicute from. rather than specifying dxguid.lib in properties, specify the full path
example: Linker->Input->AdditionalDependencies->
dinput8.lib; C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Lib\x64\dxguid.lib

2. Per above, to compile, you must have the DirectX SDK installed. Prior to Windows 8, it was distributed as a separate package, Windows 8 and later it is included in the Windows SDK. You only need the .dlls to run the executable so no SDK needs to be on the local machine.

3. It is annoying that we use callback functions. ALL of the examples put the DI functionality into a class (because it makes sense to simplify the interface) HOWEVER class members cannot be accessed outside of a class & a member function cannot be passed as a callback!! The callback approach predates c++ and is a relic of the win32 days.
SOLUTION: Usually callback functions provide a void pointer as an argument in anticipation of the requirement to pass arguments to and from a callback. If only a single argument is required, it’s pointer can be cast to void* and recast to the original data type inside the callback. When more than one argument is desired a structure can be prototyped and its pointer passed. Here we pay close attention to scope and memory existence. Additionally, if more than one object is to be instantiated (i.e. two joysticks), then we must provide a way to track each structure relative to each instance.

4. In addition to above, if multiple joystic support is needed: the devices must be enumerated and sorted to decide which is which - SEE enumerateCallback. Unfortunately, most manufactures assume only one joystick is required for each application and therefore usually do not write device specific information to each device. Rather, a product ID is used making it impossible to repeatably detect specific hardware across computers. The solution is to run an enumeration script, gather the instance GUID and use that as the unique identifier (note: it is only valid on that specific computer and may change with re-loading of hardware changes and driver updates).

#include <iostream>
#include <Windows.h> //sleep
#include "alx_hardware_joystick.h"

int main()
{
    alx::hardware::joystick::JoystickData jsData;
    alx::hardware::joystick::Joystick js(2848288176);
    alx::hardware::joystick::Joystick js2(3538687856);
    std::cout << "Hello World!\n";
    while (true) {
        js.getStatus(&jsData);
        std::cout << 
            "x1: " << jsData.lX << 
            ", y1: " << jsData.lY << 
            ", z1: " << jsData.lZ 
            << std::endl;
        js2.getStatus(&jsData);
        std::cout <<
            "x2: " << jsData.lX <<
            ", y2: " << jsData.lY <<
            ", z2: " << jsData.lZ
            << std::endl;
        Sleep(1000);
    }
}


namespace alx {
	namespace hardware {
		namespace joystick {

			struct JoystickData {
				LONG    lX;			/* x-axis position       */
				LONG    lY;			/* y-axis position       */
				LONG    lZ;			/* z-axis position       */
				LONG    lRx;			/* x-axis rotation       */
				LONG    lRy;			/* y-axis rotation       */
				LONG    lRz;			/* z-axis rotation       */
				LONG    rglSlider[2];	/* extra axes positions  */
				DWORD   rgdwPOV[4];	/* POV directions        */
				BYTE    rgbButtons[32];	/* 32 buttons            */
			};

			struct JoystickPointers {
				LPDIRECTINPUT8          pDI;				//interface pointer
				LPDIRECTINPUTDEVICE8    pDIJoystick;	//device pointer 
				DWORD* devProductId;	//unique identification of hardware
			};

			class Joystick {
				bool m_isConnected;
				alx::system::MessageQueue m_messages;
				JoystickPointers* m_jsPointers;			
			public:
				Joystick();
				Joystick(DWORD devId);
				~Joystick();
				bool isConnected();
				HRESULT getStatus(JoystickData* _state);
			};

			BOOL enumerateCallback(LPCDIDEVICEINSTANCE devInst, LPVOID pvRef);
			BOOL setPropertyCallback(const DIDEVICEOBJECTINSTANCE* pdidoi, VOID* pContext);
		}
	}
}

Note: alx::system::MessageQueue m_messages; can be found in the message queue tutorial or can be replaced with cout or any other print to console call [2]. [2] http://ipengineering.us/appdev/message.html

As stated in the notes, it is a nice to have to support multiple devices within the class structure. Thus, the default constructor is overloaded with one that takes an argument to specifying the desired device. If not specified or specified as zero, the intention is to connect to the first device found.

Joystick::Joystick() {
	Joystick(0);
}

Then in the overloaded constructor, the structure that manages the instance specific pointers is initialized. Here we use dynamic allocation such that we can pass a pointer to the structure to the callback functions.

Joystick::Joystick(DWORD devID) {
	//create unscoped structure for joystick callbacks (relic of C programming)
	m_jsPointers = new JoystickPointers;
	m_jsPointers->devProductId = new DWORD;
	*(m_jsPointers->devProductId) = devID;
	m_jsPointers->pDI = NULL;
	m_jsPointers->pDIJoystick = NULL;
	m_isConnected = false;

to avoid memory leaks, it is important to manage this memory in the deconstructor.

Joystick::~Joystick() {
	m_isConnected = false;
	//unaquire the device
	if(m_jsPointers->pDIJoystick != NULL)
		m_jsPointers->pDIJoystick->Unacquire();
	//free dynamic memory
	delete m_jsPointers->devProductId;
	delete m_jsPointers;	
}


To start using the Direct Input APIs, the DirectInput8Create function is called. Here you can see the extensive use of pointers and Windows specific data types.

HRESULT hr;								//windows typed output (success, ect..)
hr = DirectInput8Create(						//dinput8.lib
	(HINSTANCE)GetModuleHandle(0),		//instance to the console window/ calling exe if dll
	DIRECTINPUT_VERSION,
	IID_IDirectInput8,						//dxguid.lib
	(VOID**)(&(m_jsPointers->pDI)),			//pointer to m_pDI variable (pointer to the newly created DirectInput interface)
	NULL
);
if (hr != DI_OK)
	m_messages.addBack("ERROR: creating DirectInput interface.");


As shown, the fourth argument is a pointer to a pointer and has a void data type. We could just send it the address of the appropriate variable where it would automatically cast to the prototyped data type, but it is customary to put the explicit type cast for visibility. Thus (void**) is the desired data type, and &(variable) is the address we want to pass.
If a pointer to the Direct Input Interface is passed & the create function returned without error, the process to enumerate the devices (joysticks in this application) can start. This is accomplished using the EnumDevices() method.

if (m_jsPointers->pDI != NULL) {					//gets instantiated if DirectInput8Create(...) succedes 
	hr = m_jsPointers->pDI->EnumDevices(
		DI8DEVCLASS_GAMECTRL,			//which type of devices to enumerate
		enumerateCallback,					// callback for enumeration
		(LPVOID)m_jsPointers,
		DIEDFL_ATTACHEDONLY				//DIEDFL_ALLDEVICES, DIEDFL_ATTACHEDONLY
	);
	if (hr != DI_OK)
		m_messages.addBack("ERROR: cannot find joystick...");
}


Here we select the appropriate flags for specification of which type of devices we want to search for (all devices, game controllers, keyboards, etc.). Additionally, we need to specify the limitation of state of the device; that is previously used devices, currently attached devices, or devices that are available for use.
The EnumDevices method searches for devices matching the criteria and calls the specified callback for each of those matching devices.
Inside the callback we need to attempt to match each found device to the specified device info (sent as a parameter in the constructor). Then, if a match is found, the device instance is created and the associated pointer is returned (in our pointer structure in this case).

BOOL enumerateCallback(LPCDIDEVICEINSTANCE devInst, LPVOID pvRef) {
	//	recast pvRedf, use devID to find specific device, then save instance to object
	JoystickPointers* _jsParams = (JoystickPointers*)pvRef;

	if ((*(_jsParams->devProductId) == 0) ||
		(*(_jsParams->devProductId) == devInst->guidInstance.Data1)){
		//connect to this device
		HRESULT hr = _jsParams->pDI->CreateDevice(
			devInst->guidInstance,		//reference to the Gui ID
			&(_jsParams->pDIJoystick),	//address to recieve the Interface pointer
			NULL
		);
		if (FAILED(hr))					//coudnt connect
			return DIENUM_CONTINUE;
	}
	else {
		//continue through enumeration list to find specific device
		return DIENUM_CONTINUE;
	}

	//Stop enumeration, success implied.
	return DIENUM_STOP;
}

Inside the enumeration callback, we have to cast pvRef (having a void* datatype) to the appropriate type. Then compare our device info with the expected, and finally create the device if successful.

Because we passed a structure of pointers, we can over write the NULL initialized value with the created pointer to the device; and because dynamic variables have global scope the written value will persist even after the callback goes out of scope.

Lastly, when the callback returns to the constructor, the configuration of the device is similar using another callback and our structure of pointers.

#include <iostream>
#include "alx_hardware_joystick.h"

namespace alx {
	namespace hardware {
		namespace joystick {
			Joystick::Joystick() {
				Joystick(0);
			}
			Joystick::Joystick(DWORD devID) {
				//create unscoped structure for joystick callbacks (relic of C programming)
				m_jsPointers = new JoystickPointers;
				m_jsPointers->devProductId = new DWORD;
				*(m_jsPointers->devProductId) = devID;
				m_jsPointers->pDI = NULL;
				m_jsPointers->pDIJoystick = NULL;
				m_isConnected = false;

				//------1.  create a handle to direct input interface	------
				HRESULT hr;							  //windows typed output (success, ect..)
				hr = DirectInput8Create(			  //dinput8.lib
					(HINSTANCE)GetModuleHandle(0),	  //instance to the console window/ calling exe if dll
					DIRECTINPUT_VERSION,
					IID_IDirectInput8,				  //dxguid.lib
					(VOID**)(&(m_jsPointers->pDI)),		  //pointer to m_pDI variable (pointer to the newly created DirectInput interface)
					NULL
				);
				if (hr != DI_OK)
					m_messages.addBack("ERROR: creating DirectInput interface.");

				//-------2.   enumerate devices	------
				if (m_jsPointers->pDI != NULL) {					//gets instantiated if DirectInput8Create(...) succedes 
					//Device Types to enumerate
						//DI8DEVCLASS_ALL:		  All possible devices.
						//DI8DEVCLASS_DEVICE :	  All devices that do not fall into another class.
						//DI8DEVCLASS_GAMECTRL :  All game controllers.
						//DI8DEVCLASS_KEYBOARD :  All keyboards.Equivalent to DI8DEVTYPE_KEYBOARD.
						//DI8DEVCLASS_POINTER :	  All devices of type DI8DEVTYPE_MOUSE and DI8DEVTYPE_SCREENPOINTER.
					//Flags for scope of enumeration:
						//DIEDFL_ALLDEVICES :	   All installed devices are enumerated.This is the default behavior.
						//DIEDFL_ATTACHEDONLY  :   Only attached and installed devices.
						//DIEDFL_FORCEFEEDBACK :   Only devices that support force feedback.
						//DIEDFL_INCLUDEALIASES :  Include devices that are aliases for other devices.
						//DIEDFL_INCLUDEHIDDEN :   Include hidden devices.For more information about hidden devices, see DIDEVCAPS.
						//DIEDFL_INCLUDEPHANTOMS : Include phantom(placeholder) devices.
						//There are more at MSDN, also I'm not sure why there are multiple interfaces for DirectInput
					//pvRef - a void pointer used to pass arguments to callback
						//here we will pass specific info identifying the joystick we want
					hr = m_jsPointers->pDI->EnumDevices(
						DI8DEVCLASS_GAMECTRL,			//which type of devices to enumerate
						enumerateCallback,				// callback for enumeration
						(LPVOID)m_jsPointers,
						DIEDFL_ATTACHEDONLY				//DIEDFL_ALLDEVICES, DIEDFL_ATTACHEDONLY
					);
					if (hr != DI_OK)
						m_messages.addBack("ERROR: cannot find joystick...");
				}

				if (m_jsPointers->pDIJoystick != NULL) {
					//Cooperation Level: specifying DirectX how our application intends to work together with other applications
					//	Possible Options:
					//		DISCL_BACKGROUND: The application requires background access. 
					//			If background access is granted, the device can be acquired 
					//			at any time, even when the associated window is not the active window.
					//		DISCL_FOREGROUND: The application requires foreground access. If foreground 
					//			access is granted, the device is automatically unacquired when the 
					//			associated window moves to the background.
					//		DISCL_EXCLUSIVE: The application requires exclusive access. If exclusive access 
					//			is granted, no other instance of the device can obtain exclusive access to the 
					//			device while it is acquired. However, nonexclusive access to the device is always 
					//			permitted, even if another application has obtained exclusive access.
					//		DISCL_NONEXCLUSIVE: The application requires nonexclusive access. Access to the device 
					//			does not interfere with other applications that are accessing the same device.			  
					HWND hConsole = GetConsoleWindow();			//handle to the console window or dll
					hr = m_jsPointers->pDIJoystick->SetCooperativeLevel(hConsole, DISCL_NONEXCLUSIVE | DISCL_BACKGROUND);
					if (hr != DI_OK)
						m_messages.addBack("ERROR: setting cooperation level on joystick...");

					//Data Formats:
					//		c_dfDIJoystick :  Generic joystick.
					//		c_dfDIJoystick2 : Generic force feedback joystick.
					//	Picking generic joystic allows us to use the DIJOYSTATE struc to aquire data
					hr = m_jsPointers->pDIJoystick->SetDataFormat(&c_dfDIJoystick);
					if (hr != DI_OK)
						m_messages.addBack("ERROR: setting data format on joystick...");

					//get device capabilities:
					DIDEVCAPS _capabilities;
					_capabilities.dwSize = sizeof(DIDEVCAPS);
					hr = m_jsPointers->pDIJoystick->GetCapabilities(&_capabilities);
					if (hr != DI_OK) {
						m_messages.addBack("ERROR: setting getting capabilities on joystick...");
					}
					//else {
					//	std::cout << "      device has: " << _capabilities.dwAxes << " axes, "
					//		<< _capabilities.dwButtons << " buttons, "
					//		<< _capabilities.dwPOVs << " POV's, " << std::endl;
					//}

					//Set the properties for each control (button, axis, slider, ...) using case struct in a callback 
					//the scope of the enumeration can be limited:
					//	DIDFT_ALL :		All objects.
					//	DIDFT_ABSAXIS : An absolute axis.
					//	DIDFT_AXIS :	An axis, either absolute or relative.
					//	DIDFT_BUTTON :	A push button or a toggle button.
					//	DIDFT_POV :		A point-of-view controller.
					hr = m_jsPointers->pDIJoystick->EnumObjects(setPropertyCallback, (void*)m_jsPointers, DIDFT_ALL);
					if (hr != DI_OK)
						m_messages.addBack("ERROR: setting up joystick objects...");

					//report out
					m_messages.addBack("Joystick Connected...");
					m_isConnected = true;

				
				}
			}
			Joystick::~Joystick() {
				m_isConnected = false;
				//unaquire the device
				if(m_jsPointers->pDIJoystick != NULL)
					m_jsPointers->pDIJoystick->Unacquire();
				//free dynamic memory
				delete m_jsPointers->devProductId;
				delete m_jsPointers;	
			}
			bool Joystick::isConnected() {
				return m_isConnected;
			}
			HRESULT Joystick::getStatus(JoystickData* _state) {
				HRESULT hr;							  //windows typed output (success, ect..)
				DIJOYSTATE _diState;						  // DInput Joystick state
				if (NULL == m_jsPointers->pDIJoystick)
					return S_OK;
				// Poll the device to read the current state
				hr = m_jsPointers->pDIJoystick->Poll();
				if (FAILED(hr))
				{
					m_isConnected = false;
					// DInput is telling us that the input stream has been
					// interrupted. We aren't tracking any state between polls, so
					// we don't have any special reset that needs to be done. We
					// just re-acquire and try again.
					hr = m_jsPointers->pDIJoystick->Acquire();
					if (hr == DIERR_INPUTLOST)
					{
						hr = m_jsPointers->pDIJoystick->Acquire();
						if (hr == DI_OK)
							m_isConnected = true;
						else {
							m_messages.addBack("ERROR: reconnecting to joystick...");
							return hr;
						}
					}
					// hr may be DIERR_OTHERAPPHASPRIO or other errors.  This
					// may occur when the app is minimized or in the process of 
					// switching, so just try again later 
					return S_OK;
				}
				hr = m_jsPointers->pDIJoystick->GetDeviceState(sizeof(DIJOYSTATE), &_diState);
				// Get the input's device state
				if (FAILED(hr))
				{
					m_messages.addBack("ERROR: updating Joystick state..." );
					m_isConnected = false;
					return hr;
				}
				//std::cout << _diState.lX << " " << _diState.lY << " " << _diState.lRz << std::endl;
				_state->lX = _diState.lX;
				_state->lY = _diState.lY;
				_state->lZ = _diState.lZ;
				_state->lRx = _diState.lRx;
				_state->lRy = _diState.lRy;
				_state->lRz = _diState.lRz;
				_state->rglSlider[0] = _diState.rglSlider[0];
				_state->rglSlider[1] = _diState.rglSlider[1];
				_state->rgdwPOV[0] = _diState.rgdwPOV[0];
				_state->rgdwPOV[1] = _diState.rgdwPOV[1];
				_state->rgdwPOV[2] = _diState.rgdwPOV[2];
				_state->rgdwPOV[3] = _diState.rgdwPOV[3];
				memcpy(&(_state->rgbButtons[0]), &(_diState.rgbButtons[0]), 32);
				return hr;
			}

			BOOL enumerateCallback(LPCDIDEVICEINSTANCE devInst, LPVOID pvRef) {
				//this function is called if a divice is found durring enumeration
				//	a reference to the device is passed in via devInst.
				//	recast pvRedf, use devID to find specific device, then save instance to object
				JoystickPointers* _jsParams = (JoystickPointers*)pvRef;

				//for debug, output info
				std::wcout << "Joystick found-> Instance Name: " << devInst->tszInstanceName <<
					", Product ID: " << devInst->guidInstance.Data1 << std::endl;
				if ((*(_jsParams->devProductId) == 0) ||
					(*(_jsParams->devProductId) == devInst->guidInstance.Data1)){
					//connect to this device
					HRESULT hr = _jsParams->pDI->CreateDevice(
						devInst->guidInstance,		//reference to the Gui ID
						&(_jsParams->pDIJoystick),	//address to recieve the Interface pointer
						NULL
					);
					// If it failed, then we can't use this Joystick. (Maybe the user unplugged
						// it while we were in the middle of enumerating it.)
					//note: that DirectInput allows us to continue the enumeration for as
						//long as we want, as the callback function must return DIENUM_CONTINUE, to continue 
						//the enumeration, or DIENUM_STOP to stop the enumeration.
					if (FAILED(hr))
						return DIENUM_CONTINUE;
				}
				else {
					//continue through enumeration list to find specific device
					return DIENUM_CONTINUE;
				}

				//Stop enumeration, success implied.
				return DIENUM_STOP;
			}
			
			BOOL setPropertyCallback(const DIDEVICEOBJECTINSTANCE* pdidoi, VOID* pContext) {
				//recast pContext to pointer structure
				JoystickPointers* _jsPointers = (JoystickPointers*)pContext;

				// For axes that are returned, set the DIPROP_RANGE property for the
				// enumerated axis in order to scale min/max values.
				if (pdidoi->dwType & DIDFT_AXIS)
				{
					DIPROPRANGE diprg;
					diprg.diph.dwSize = sizeof(DIPROPRANGE);
					diprg.diph.dwHeaderSize = sizeof(DIPROPHEADER);
					diprg.diph.dwHow = DIPH_BYID;
					diprg.diph.dwObj = pdidoi->dwType; // Specify the enumerated axis
					diprg.lMin = -100;
					diprg.lMax = 100;

					// Set the range for the axis
					if (FAILED(_jsPointers->pDIJoystick->SetProperty(DIPROP_RANGE, &diprg.diph)))
						return DIENUM_STOP;
				}

				return DIENUM_CONTINUE;
			}
		}
	}
}